---
tags: Vue.js 設計與實現, 讀書會
---
# Vue.js 設計與實現

ISBN-13:9787115583864
# 第零章 註解
本心得由於是參考至簡體書之版本,因內文技術相關用語與臺灣常用計算機用語有所差異,而筆者心得將會參考原文中之用詞並譯成英文,再將英文用語對照翻譯成臺灣常用計算機用語的版本,在此羅列紀錄供讀者參考。
## 對照表
|原文中|依原文而譯|依英文而譯|
|---|---|---|
|聲明式|declarative|宣告式|
|命令式|imperative|指令式|
|運行時|Runtime|執行期|
|編譯時|Compile-time|編譯期|
|組件|component|元件|
|模塊|module|模組|
|函數|function|函式|
|對象|object|物件|
|回調|callback|回呼|
|調度|schedule|排程|
## 翻譯參考資源
- [大陸台灣計算機術語對照表](https://zh.wikibooks.org/zh-tw/%E5%A4%A7%E9%99%86%E5%8F%B0%E6%B9%BE%E8%AE%A1%E7%AE%97%E6%9C%BA%E6%9C%AF%E8%AF%AD%E5%AF%B9%E7%85%A7%E8%A1%A8)
# 第一章 權衡藝術
作為框架設計者,要瞭解框架定位與方向,尤其對於模組拆分來說很重要。
作為框架學習者,要瞭解框架理念,撰寫思路才會流暢。
整體來說,先告訴你為什麼要這樣選,後面怎麼做只是實現方式。
## 1.1 指令式(imperative)與宣告式(declarative)
### 概念
越偏向高階、人類社會的行為準則 => 宣告式
越接近計算機執行過程 => 指令式
兩者為相較概念而非絕對概念
### 差異
越是上層(偏向宣告式)意味著下層要處理的事情更多,也就是效能上的耗損。
越是下層(偏向指令式)意味著有更多操作空間,但相對不好理解。
### 實作
- 偏向指令式 https://codepen.io/ShawnLin0201/pen/xxaaobJ
- 偏向宣告式 https://codepen.io/ShawnLin0201/pen/GRXXaxe
### 範例:以 uber 為例:
指令式:告訴司機下個路口左轉,前進一百公尺,下個路口右轉。
宣告式:告訴司機,我要去台北車站。
## 1.2 性能與可維護性權衡
宣告式程式碼性能**不優於**指令式程式碼,宣告式程式碼性能**最多做到相等於**指令式程式碼。
### 1.2.1 性能比較
A => 直接修改的性能消耗
B => 找出差異的性能消耗
指令式程式碼,更新性能消耗 = A
宣告式式程式碼,更新性能消耗 = A+B
### 1.2.2 可維護性比較
#### 對設計者
指令式:需關注使用者撰寫可能會遇到的問題
宣告式:要暴露什麼樣的介面與減少性能損耗
#### 對使用者
指令式:關注整體過程
宣告式:關注業務邏輯
從產品角度看 Vue 框架定位:想讓使用者關注其業務邏輯 => 宣告式是其作法
## 1.3 虛擬 DOM 性能
總結來說,操作虛擬 DOM 性能理論上肯定比原生操作差,原因在於很難寫出絕對優化極致的程式碼,即便能達成投入產出比並不高。
### 1.3.1 innerHTML / 虛擬 DOM 之間性能與選擇的理由
既然 Vue 為宣告式框架,那麼降低性能損耗就是設計者其中一個關注要點。
而虛擬 DOM 的做法目的就是最小化性能損耗的選擇作法。
在更新視覺層可選的做法有:
- innerHTML
- Virtual DOM
- 原生 JavaScript
#### 虛擬 DOM 實作
實作:https://codepen.io/ShawnLin0201/pen/zYJmbxP
參考:https://github.com/livoras/blog/issues/13
#### 對於創造頁面來說
||innerHTML|Virtual DOM|
|--|--|--|
|JavaScript 計算|組 HTML String(Template)|建立 VNode|
|DOM 計算|新建所有 DOM|新建所有 DOM|
兩者在創造同個數量級別的 DOM 時性能幾乎相等。
#### 對於更新頁面來說
||innerHTML|Virtual DOM|
|--|--|--|
|JavaScript 計算|組 HTML String(Template)|建立 VNode + Diffing|
|DOM 計算|銷毀原先所有 DOM,再新建所有 DOM|僅更新必要的 DOM|
JavaScripth Runtime 層運算中,Virtual DOM 多了 Diff 消耗,然而因為是 JavaScript Runtime 層的運算,所以數量上的差異並不影響其消耗。
然而在 DOM 計算當中,Virtual 只更新了必要的 DOM,而 innerHTML 會做全面的更新,因此數量級別越大則相較消耗得越多。
#### 維度分析
||innerHTML|Virtual DOM|原生 JavaScript|
|--|--|--|--|
|心智負擔|普通,拼接字串|小|大,需手動 CRUD 大量的 DOM|
|可維護性|強||差|
|性能|差,全更新|中等,因為 Diff 環節的比較|性能高|
問:書中為何說 Virtual DOM 心智負擔小?innerHTML 在 template 表現也不差
對比:https://codepen.io/ShawnLin0201/pen/oNPaOqx
## 1.4 編譯期(Compile-time)/執行期(Runtime)
### 執行期
提供 Render 函數情況下,使用時使用者仍須手動寫樹狀結構的模板物件
```javascript
const obj = {
tag:'div',
children:{
{tag:'span',children:'hello world'}
}
}
function Render(obj,root){
const el = document.createElement(obj.tag)
if(typeof obj.children === 'string'){
const text = document.createTextNode(obj.children)
el.appendChild(text)
}else{
obj.children.forEach((child)=>Render(child,el))
}
root.appendChild(el)
}
Render(obj, document.body)
```
沒編譯過程,沒辦法分析使用者撰寫的內容(缺點)
### 編譯期+執行期
藉由 Compiler 將字串模板 HTML 轉為 Render 函式能處理的格式
```javascript=
const template = `
<div>
<p>Hello, world!</p>
</div>
`
const obj = Compiler(template)
Render(obj, document.body)
```
由於有編譯步驟,可以分析使用者寫的內容(優點)
### 編譯期
只用 Compiler 將字串模板直接轉化為指令式程式碼
由於有編譯步驟,可以分析使用者寫的內容(優點)
由於直接編寫成指令式程式碼,性能可能會更好(優點)
程式碼必須一定要編譯過才能用,非可選擇(缺點)
Svelte 即是純編譯框架,但暫時可能達不到理論中的性能高度。
## 1.5 結論
作為要使開發體驗良好的框架,Vue 選擇要暴露出宣告式的 API,而面對相較指令式框架效能差異的問題,則是透過編譯期的處理,盡可能降低封裝所帶來的負擔,並且仍然保留了不需編譯仍然能使用的可能性。
# 第二章 框架設計的核心要素
## 2.1 提升用戶開發體驗
### 2.1.1 警告訊息
以框架、工具開發者角度,若預期框架、工具使用者使用不當會產生狀況,因適當提供對應的錯誤訊息來排解錯誤。
反思:但是身為開發產品工程師來說,會有錯誤時其實應該考量使用者退路。(這點在寫測試時的考量就會截然不同!)
### 2.1.2 直觀的輸出
因為框架本身可能為了實現某項功能,因此必須得封裝某些變數,所以開發過程中若使用者想要查看這些封裝過的變數時可能會有些困擾。
```javascript=
const count = ref(0)
console.log(count.value) // 0
```
使用瀏覽器自定義的 formatter,就能夠解決這問題,**但前提是「使用者要知道」**。
自訂方法:https://www.mattzeunert.com/2016/02/19/custom-chrome-devtools-object-formatters.html
## 2.2 控制框架體積
控制體積的方法 => feature toggle 概念
feature toggle 概念實作 => 加入常數 `__DEV__` 判斷,並透過打包工具做 `Dead Code` 的 Tree-Shaking
```js
if(__DEV__ && targetFeature){
// about target feature implementation
}
```
## 2.3 Tree-Shaking
### 2.3.1 Tree-Shaking 限制
必須為 **ESM(ES Module)**,因為 Tree-Shaking 必須依賴 ESM 的 **靜態結構** 來分析。
### 2.3.2 Tree-Shaking 實作 via Rollup
安裝 Rollup
```shell=
npm install rollup -D
```
input.js
```javascript=
import {foo} from './utils.js'
foo()
```
utils.js
```javascript=
export function foo (obj) {
obj && obj.foo
}
export function bar (obj) {
obj && obj.bar
}
```
執行
```shell=
npm rollup input.js -f esm -o bundle.js
```
輸出結果
```javascript=
function foo (obj){
obj && obj.foo;
}
foo();
```
### 2.3.2 探討副作用
單純依賴 ESM 做靜態結構分析的後果就是有副作用的函式會無法得知最後能不能被清除。
```javascript=
import { foo } from './utils.js'
foo() // 以靜態分析來說有使用到該函式
```
但實際上實作只有在 Run-Time 階段才會知道,因此無法確認這段是不是能刪除。
```javascript=
export function foo (obj) {
obj && obj.foo
}
```
### 2.3.3 透過註解強迫處理
針對上面討論到的副作用,打包工具通常會提供對應的機制來解決。
如 Rollup.js 中的 `/*#__PURE__*/`
```javascript=
import { foo } from './utils.js'
/*#__PURE__*/ foo() // 這段程式碼已經被標示不會產生副作用
```
再次執行打包後就會看到已經被清除了。
```javascript=
// bundle.js
// 空的
```
## 2.4 框架輸出的建置(build)產物
前面已經有提過錯誤訊息應該只應於開發環境,因此基於環境上就可以區分為用於正式環境(production)與開發環境(development)兩種版本:
開發環境:vue.global.prod.js
正式環境:vue.global.js
但實際上還會根據使用場景不同以及建置階段不同而區分成下列四種。
### 2.4.1 HTML 引入 script
使用情境:
```htmlmixed
<body>
<script src="/path/to/vue.js"></script>
<script>
const {createApp} = Vue
//...
</script>
</body>
```
針對這種情境會輸出 IIFE(Immediately Invoked Function Expression,立即函式)的形式。
而實際上 vue.global.js 的檔案正是 IIFE 形式的資源:
```javascript
var Vue = (function(exports){
//...
exports.createApp = createApp;
//...
return exports
}({}))
```
如此一來只要引入該檔案就能直接在 HTML 中使用了。
而要生成這個檔案只需要在 rollup.js 中設定 `format: 'iife'` 即可輸出成這種形式。
```javascript
const config = {
input:'input.js',
output:{
file:'output.js',
format:'iife' //指定模块形式
}
}
export default config
```
### 2.4.2 HTML 引入 ESM
使用情境:
```htmlmixed
<script type="module" src="/path/to/vue.esm-browser.js"></script>
```
而要生成這個檔案只需要在 rollup.js 中設定 `format: 'esm'` 即可輸出成這種形式。
```javascript
const config = {
input:'input.js',
output:{
file:'output.js',
format:'esm' //指定模块形式
}
}
export default config
```
而 Vue.js 中最終輸出的檔案為 `vue.esm.browser.js`。
### 2.4.3 會經打包工具後的 ESM
使用情境同樣為 ESM 但主要使用者會再經由建構工具產生最終檔案。
而 Vue.js 中最終輸出的檔案為 `vue.esm.bundler.js`。
與 `vue.esm.browser.js` 不同的是原先會透過打包工具控制 `__DEV__` 來實現 Tree-Shaking 的機制,而建構工具則是需要透過 `(process.env.NODE_ENV !== 'production')` 才能達成。
目的同樣在於將控制權交由使用者自行決定生產環境。
### 2.4.4 SSR
使用情境:讓使用者能在 node 環境中引用資源,因此要輸出 `cjs` 模組。
```node
const Vue = require('vue')
```
而要生成這個檔案只需要在 rollup.js 中設定 `format: 'cjs'` 即可輸出成這種形式。
```javascript
const config = {
input:'input.js',
output:{
file:'output.js',
format:'cjs' //指定模块形式
}
}
export default config
```
## 2.5 特性切換(feature toggle)
### 2.5.1 何謂 feature toggle
提供 A、B、C 等多種功能而讓使用者可自行決定要開關哪些功能。
思考點
- 將其與 Tree-shaking 機制綁定,可減少最終體積
- 對設計帶來彈性而不需擔心增加功能等同增加體積(on-demand)
- 工具將來需要使用者 migration 時,可選擇遺留哪些既有 API
### 2.5.2 feature toggle 實作
與 `__DEV__` 類似,透過 `__FEATURE_OPTIONS_API__` 來控制:
```javascript
{
__FEATURE_OPTIONS_API__:isBundlerESBuild ? `__VUE_OPTIONS_API__`:true
}
```
若最終建構資源是由打包工具所產生的時候,就可以透過 `__VUE_OPTIONS_API__` 設定是否要保留 `options API` 功能,若明確知道不會使用到則可以關閉該功能,減少最終打包體積。
## 2.6 錯誤處理
設計:DRY
設計:Open/Close Principle
```javascript
let handleError = null
export default{
foo(fn) {
callwithErrorHandling(fn)
},
boo(fn) {
callwithErrorHandling(fn)
},
// 使用者可以透過此定義錯誤函式要如何處理
registerErrorHandler(fn){
handleError = fn
}
},
function callwithErrorHandling(fn){
try {
fn && fn()
} catch (e) {
// 捕獲錯誤
handleError(e)
}
}
```
最終使用者使用情境:
```javascript
import utils from 'utils.is'
utils.registerErrorHandler((e)=>{
console.log(e)
})
utils.foo(() => {/*...*/})
utils.bar(() => {/*...*/})
```
## 2.7 良好的 TypeScript 支援
簡單來說,有用 TypeScript 不代表就是良好支援(也就是所謂的 AnyScript),而要「良好的」TypeScript 支援意味著需要付出不少心力才能達成。
## 2.8 結論
產品設計始終要優先考量到使用者的使用情境與痛點,再來才有額外的服務。
而作為框架在使用的業務場景中,「開發提示」、「體積」、「輸出檔案格式」是必須優先考量的內容。
其中框架體積的部分,需要做到增加功能的同時,又要讓使用者可選自己所需要的,而非將所以功能都打包後放入到終端使用者的環境當中。因此,選擇透過 Tree-shaking 來達成這一目標,然而要透過這方式的必要條件便是引入模組格式得採用 ESM 進行開發。
最後此書稍稍認真提到,所謂「良好的 TypeScript 支援」是要耗費相當大的心力才能做到的,而這一切都是為了提升開發者在開發提示的使用體驗,並不是外在看起來這麼觸手可及的。
# 第三章 設計思路
開發一個具有規模的工具、網站時,各個功能環節將會環繞著「核心思路」進行開發與規劃,進而才拓展成現在的模樣。
## 3.1 宣告式地描述UI
前端頁面必要的內容:
- 元素(element):`<h1></h1>`, `<a></a>` ..., etc.
- 屬性(attribute):`<h1 id="..."></h1>`, `<a href="..."></a>`..., etc.
- 事件(event):`click`, `keydown`..., etc.
- 元素間的階層結構(parent/child node):`<div><p></p></div>`
宣告式地描述UI(Vue 方案):
- 元素(element):採用 `HTML` 標籤一致的做法來描述 `DOM`
- 靜態屬性(attribute):採用 `HTML` 標籤一致的做法來描述。
- 動態屬性(attribute):透過 `:`, `v-bind` 來描述 `<a :href="dynamic"></a>`
- 事件(event):透過 `@`, `v-on` 來描述 `<div @click="handleOnClick">button</div>`
- 元素間的階層結構(parent/child node):採用 `HTML` 標籤一致的做法。
盡可能地採用 `HTML` 規範目的主要也在於要讓使用者開發體驗良好,讓使用者盡可能不需要額外多學習不必要的內容。
### 描述方式
#### 透過 JavaScript 物件描述 UI
```javascript
const card = {
tag: 'div',
props: {
onClick: handleOnClick
},
children: [
{
tag: 'p',
children: 'Header'
},
{
tag: 'div',
children: 'Content'
}
]
}
```
#### 透過模板描述 UI
```htmlmixed
<div>
<p>Header</p>
<div>Content</div>
</div>
```
兩者差異在於:
- 透過 JavaScript 物件描述 UI 更加靈活
- 透過模板描述 UI 更加直覺
### 物件描述與虛擬 DOM 結合
```javascript
import {h} from 'vue'
export default {
render() {
return h('h1', { onClick: handleOnClick })
/* 等價於
{
tag:'h1',
props: {
onClick: handleOnClick
}
}
*/
}
}
```
其返回的內容即是 JavaScript 物件描述的 UI,而 `render` 函式則是供 `Vue` 介面渲染所使用。
## 3.2 渲染器
而上述的物件描述要渲染到畫面上,主要便是依靠渲染器(renderer)來執行:
```html
h('h1', 'hello, world.') -> renderer -> 實際 DOM
```
假設我們有一虛擬 DOM:
```javascript
const vnode = {
tag: 'div',
props: {
onClick: () => alert('hello world')
},
children: 'click'
}
```
接著寫一個渲染器,目的是把上方的虛擬 DOM 渲染為真實的 DOM:
```javascript
function renderer(vnode, container){
const el = document.createElement(vnode.tag)
for(const key in vnode.props){
if(/^on/.test(key)){
el.addEventListener(
key.substr(2).toLowerCase(),
vnode.props[key]
)
}
}
if(typeof vnode.children === 'string'){
el.appendChild(document.createTextNode(vnode.children))
} else if(Array.isArray(vnode.children)){
vnode.children.forEach(child => renderer(child, el))
}
container.appendChild(el)
}
```
而 `renderer` 函數主要的參數:
- `vnode`:虛擬 DOM
- `container`:實體 DOM 元素,用意是讓虛擬 DOM 渲染至該節點上
最後使用場景:
```javascript
renderer(vnode, document.body)
```
實作:https://codepen.io/ShawnLin0201/pen/poxbbXB
## 3.3 元件的本質
元件的本質就是一組 DOM 元素的封裝:
```javascript
const Component = function() {
return {
tag: 'div',
props: {
onClick: () => alert('hello world')
},
children: 'click'
}
}
```
接著我們將其轉為虛擬 DOM 物件的形式來儲存:
```javascript
const vnode = {
tag: Component
}
```
現在渲染器需要支援元件的狀況,因此需要改寫 3.2 所提到的渲染器:
```javascript
function renderer(vnode, container){
if(typeof vnode.tag === 'string'){
mountElement(vnode, container)
} else if(typeof vnode.tag === 'function'){
mountComponent(vnode, container)
}
}
```
接著處理普通標籤,這邊與原先的處理相同:
```javascript
function mountElement(vnode, container){
const el = document.createElement(vnode.tag)
for(const key in vnode.props){
if(/^on/.test(key)){
el.addEventListener(
key.substr(2).toLowerCase(),
vnode.props[key]
)
}
}
if(typeof vnode.children === 'string'){
el.appendChild(document.createTextNode(vnode.children))
} else if(Array.isArray(vnode.children)){
vnode.children.forEach(child => renderer(child, el))
}
container.appendChild(el)
}
```
再來要處理元件的情境:
```javascript
function mountComponent(vnode, container){
const subtree = vnode.tag()
renderer(subtree, container)
}
```
最終使用場景:
```javascript
renderer(vnode, document.body)
```
實作:https://codepen.io/ShawnLin0201/pen/WNaxGwP
### 元件轉為渲染函式,再透過渲染函式渲染
假設今天元件形式並非為函式而是物件描述:
```javascript
const Component = {
render() {
return {
tag: 'div',
props: {
onClick: () => alert('hello world')
},
children: 'click'
}
}
}
```
接著要修改原先渲染器的判斷條件:
```javascript
function renderer(vnode, container){
if(typeof vnode.tag === 'string'){
mountElement(vnode, container)
} else if(typeof vnode.tag === 'object'){ // 改為判斷為物件
mountComponent(vnode, container)
}
}
```
接著原先 `mountComponent` 函式也要調整成:
```javascript
function mountComponent(vnode, container){
const subtree = vnode.tag.render()
renderer(subtree, container)
}
```
最終使用場景依然是相同的:
```javascript
renderer(vnode, document.body)
```
實作:https://codepen.io/ShawnLin0201/pen/oNaLzBe
## 3.4 模板原理
為了讓開發者更方便使用,因此 Vue 除了物件描述 UI 也同時支援提供模板描述 UI:
```htmlmixed
<div @click="handleOnClick">
click
</div>
```
而這部分主要是由編譯器(compiler)所支援的部分,編譯器將會把上述模板編譯為與之功能相同的渲染函式:
```javascript
var vnode = {
render(){
return h('div', { onClick: handleOnClick }, 'click')
/* 等價於
{
tag: 'div',
props: {
onClick: handleOnClick
},
children: 'click'
}
*/
}
}
```
而單文件 `.vue` 中的 `<template>` 標籤中的內容就是模板內容:
```htmlmixed
<template>
<div @click="handleOnClick">
click
</div>
</template>
<scripts>
export default {
methods: {
handleOnClick: () => alert('hello world!')
}
}
</scripts>
```
編譯器主要就是根據模板內容編譯成渲染函式並加入到 `<script>` 當中:
```javascript
export default {
methods: {
handleOnClick: () => alert('hello world!')
},
render() {
return h('div', { onClick: handleOnClick }, 'click')
}
}
```
所以無論是使用模板或寫渲染函式,最終都會轉為渲染函式,在經過渲染器將渲染函式返回的虛擬 DOM 渲染成實體的 DOM,這除了是模板的原理之外,同時也是 Vue 本身渲染頁面的原理。
## 3.5 Vue 是由各個模組的有機體
元件依賴於渲染器、模板依賴於編譯器,而編譯後的程式碼則是根據渲染器與虛擬 DOM 而決定的,因此 Vue 模組之間是相互關聯的。
以編譯器與渲染器為例:
```htmlmixed
<div :class="cls"></div>
```
根據上文得知,編譯器應該會將上面代碼編譯成:
```javascript
render(){
tag: 'div',
props: {
class: cls
}
}
```
其中 `cls` 可能會發生變化,因此渲染器得針對這個更動而做「尋找」與「更新」,而「尋找」這件事情是耐人尋味的一件事情。
而其中一種做法就是在編譯過程中標示動態屬性:
```javascript
render(){
tag: 'div',
props: {
class: cls
},
patchFlag: 1 // 表示 class 為動態屬性
}
```
此時渲染器就能夠快速得知有哪些內容會是動態的資訊,省下「在渲染過程中」尋找時的效能。
# 第四章 響應系統作用與實作
討論何謂響應式資料與副作用函式,並且實作一個相對完整的響應系統,從中探討可能會遇到的:
- 如何避免無限遞迴
- 為什麼需要嵌套副作用函式
- 兩個副作用函式之間的影響
詳細討論響應式資料相關的內容:
- 採用 Proxy 實現
- 如何根據語言規範實現資料物件的代理
## 4.1 響應式資料與副作用函式
## 4.2 實作基本的響應式資料
## 4.3 設計一個完善的響應系統
### 4.3.A 前置學習:https://segmentfault.com/a/1190000022936727
- Proxy
- Set
- Map
- WeakMap
#### 4.3.A.1 Proxy
#### 4.3.A.2 Set
- add():新增成員
- delete():刪除成員,若刪除成功會返回 `true`
- has():查詢是否含有此成員
- size:返回
- keys():回傳迭代後的鍵名
- values():回傳迭代後的鍵值
- entries():回傳迭代後的鍵值對
- forEach():可使用 callback function 迭代每個成員
##### Set 中的值總是唯一值,不論 add 多少成員進去。
```javascript
const s = new Set()
[3, 4, 5, 4, 5, 2, 2].forEach((x) => s.add(x))
for (let i of s) {
console.log(i)
}
// 3 4 5 2
```
##### Set 中的特殊值
- +0 === -0 ,Set 視為同一值
- undefined === undefined,Set 視為同一值
- NaN !== NaN,但是在 Set 中視為同一值
##### Set 與 Array 對比
- Set.has 比 Array.prototype.indexOf 效率高
- Set 不含有重複值
- Set 能直接透過 delete 刪除某值較為方便
- Set 沒有 Array 後續 ES+ 的新方法 map、filter、some、every(需要先將 Set 轉為 Array 才能使用)
##### Set 轉換 Array
```javascript
[...new Set(array)]
Array.from(new Set(array))
```
##### 透過 Set 實作聯集(Union)、交集(Intersect)、差集(Difference)
```javascript
const a = new Set([1, 2, 4])
const b = new Set([1, 3, 5])
const union = new Set([...a, ...b])
// Set {1, 2, 4, 3, 5}
const intersect = new Set([...a].filter((x) => b.has(x)))
// set {1}
const differenceA = new Set([...a].filter((x) => !b.has(x)))
// Set {2, 4}
const differenceB = new Set([...b].filter((x) => !a.has(x)))
// Set {3, 5}
```
#### 4.3.A.3 WeakSet
與 Set 差異在於:
- 成員屬於 weak reference,可以被瀏覽器 GC:用來保存 DOM 較不易有 memory weak 問題
- 不可以被迭代,因此不能用於 for of 迴圈
- 沒有 size 屬性可使用
#### 4.3.A.4 Map
- set(key, value)
- get(key)
- delete(key)
- has(key)
- clear()
- keys():回傳迭代後的鍵名
- values():回傳迭代後的鍵值
- entries():回傳迭代後的鍵值對
- forEach():可使用 callback function 迭代每個成員
#### 4.3.A.5 WeakMap
與 Map 差異在於:
- 除了 `null` 之外只接受物件類型作為 key name
- 鍵名是 weak reference,所指向的物件會被瀏覽器 GC
- 不可以被迭代
### 4.3.1
## 4.4 分支切換與 Cleanup
## 4.5 嵌套、Effect、Effect Stack
## 4.6 避免無限遞迴
## 4.7 調度執行
## 4.8 計算屬性(computed)與 Lazy
## 4.9 watch 實現原理
## 4.10 立即執行的 watch 與回呼(callback)執行時機
## 4.11 過期的副作用