Vuejs設計與實現 —— 實現響應式系統

語言: CN / TW / HK

theme: smartblue

前言

若你想了解虛擬 DOM 的部分,可見 Vuejs設計與實現 —— 為什麼需要虛擬 DOM

Vuejs 三大核心模組: - Compiler 模組:涉及 AST 抽象語法樹的內容,再通過 generateAST 生成渲染函式等 - Runtime 模組:也稱為 Renderer 模組,將虛擬 DOM 生成真實 DOM 元素,並渲染到瀏覽器上 - Reactivity 模組:響應式系統,將 JavaScript 物件代理為資料模型,而當你修改資料模型時,檢視會進行更新

其中響應式系統是 Vuejs 中的重要組成部分,相信這一部分大家都是深有體會的,下面就嘗試實現響應式系統,目的是為了更好的瞭解響應式系統的設計與實現的過程。

響應式資料 & 副作用函式

什麼是副作用函式?

顧名思義,副作用函式指的就是會產生 副作用函式

其中的 函式 不難理解,那麼 副作用 是什麼呢?

下面舉個栗子,存在如下兩個函式(具體作用看註釋):

```js // 設定 body 中文字內容 function setTextForBody(text = 'hello vue3'){ document.body.innerText = text }

// 獲取 body 中的文字並輸出 function getTextFromBody(){ console.log("document.body = ", document.body.innerText) } `` 當函式setTextForBody函式執行時,會將頁面中body的內容預設設定為'hello vue3',而getTextFromBody函式是負責獲取body的文字內容。那麼當setTextForBody呼叫時傳入的引數不同,會導致getTextFromBody函式中獲取到的內容也會發生改變,甚至是在其他函式中也有類似的設定或讀取body中文字內容的操作,都會 **受到直接或間接的影響**,那麼就可以稱這個函式(這裡是setTextForBody` 函式)產生了 副作用(effect)

什麼是響應式資料?

請看下面的栗子: ```js // 初始資料 const data = { text: 'hello world' }

// 副作用函式 function effect(){ document.body.innerText = data.text }

// 修改資料 setTimeout(() => { data.text = 'hello vue3' }, 3000); `` 如上程式碼中,副作用函式effect會設定body的文字內容為資料data物件中的text屬性,而setTimeout則負責 3s 後將data.text` 的值進行修改。

期望的是,當代碼執行 data.text = 'xxx' 的程式碼時,副作用函式 effect 可以自動執行,而省略手動呼叫的過程,那如果能實現這個目標,那麼就可以將 data 物件稱為響應式資料。

響應式資料的基本實現

通過上面的程式碼,不難發現(畢竟程式碼量很少): - 當副作用函式 effect 執行時,會通過 data.text 進行 讀取操作 - 當需要修改 text 欄位值時,會通過 data.text = xxx 進行 設定操作 其中 讀取操作設定操作 正好對應 JavaScript 中的 gettersetter,在 Vue.js 2 中採用的是 Object.defineProperty 函式進行實現,而在 Vue.js 3 中已經轉向 Proxy 的實現方式。

基本思路

  • 將原始資料進行代理,實現 gettersetter 函式
  • 當執行副作用 effect 函式時,會觸發對應資料的 getter 函式,此時將這個 effect 函式儲存到容器 bucket 中,等待在未來某時刻執行
  • 當執行 data.text = xxx 操作時,會觸發對應資料的 setter 函式,此時從容器 bucket 中取出所有 effect 函式並執行它們

```js // 儲存副作用函式的容器 const bucket = new Set()

// 原始資料 const rawData = { text: 'hello world' }

// 副作用函式 function effect(){ document.body.innerText = proxyData.text }

// 響應式函式 function reactive(target){ return new Proxy(target, { get(target, key){ // 儲存副作用函式 effect bucket.add(effect)

    // 返回訪問的值
    return target[key]
  },
  set(target, key, newValue){
    // 設定新值
    target[key] = newValue

    // 從容器 bucket 中取出 effect 函式並執行
    bucket.forEach(fn => fn())

    // 表示設定成功
    return true
  }
})

}

// 對原始資料進行代理 const proxyData = reactive(rawData) 現在可以使用下面的程式碼來進行測試:js // 初始化執行,觸發 getter 函式,收集 effect effect()

// 2s 後對 proxyData.text 進行修改 setTimeout(() => { console.log('定時器執行,觸發修改') proxyData.text = 'hello vue3' }, 2000) ``` 得到的結果如下:

完善響應式系統

明確副目標物件和作用函式關係

存在缺陷

上面通過硬編碼的形式進行的實現,存在著如下的缺陷: - 強制副作用的函式名為 effect,這會導致一旦產生副作用的函式名不是 effect,那麼上述的程式碼實現就無法達到預期的效果 - 最佳實現 應該是哪怕副作用函式是匿名的,也能被正確的進行收集到容器中,從而在未來某個班時刻被執行 - 僅僅使用 set 資料結構作為副作用函式的容器,會導致 副作用函式和被操作目標的欄位之間無法建立明確的關係 - 目前的實現是將所有的副作用函式全部放到同一個容器中進行儲存,導致的結果就是一旦某個被操作的目標欄位進行更新操作,這會導致容器中的所有的副作用(即使是無關的)全部都會執行一遍 - 最佳實現 應該是隻執行和當前被操作目標中具體欄位相關的副作用函式,可以是一個或多個

完善思路

  • 由於期望的副作用函式可以是任意形式的函式,因此需要一個全域性變數 activeEffect 儲存當前被註冊的副作用函式
  • 為了 副作用函式和被操作目標的欄位之間建立明確的關係,需要使用 WeakMap、Map、Set 重構對應的資料結構
    • 通過 WeakMap 建立依賴容器 bucket,它的鍵是對應不同的被操作目標物件,它的值的型別是一個 Map 物件,這個 Map 物件的鍵是對應被操作目標物件中的不同欄位,其中每個欄位對應的值是 Set 資料結構,裡面可以儲存多個對應的副作用函式
    • 可以通過下圖進行輔助理解

【關於資料結構選擇上的解釋】:使用 WeakMap 用來儲存不同的被操作目標物件,是因為 WeakMap 中的鍵是弱引用的,簡單的說一旦外部環境沒有對這個目標物件的引用,那麼垃圾回收機制可以正常進行回收;而 Map 的鍵屬於強引用,即便外部沒有對目標物件的引用,但這個 Map 本身的鍵也會被認為是對目標物件的引用,因此會導致垃圾回收無法正常進行。

具體程式碼實現

```js // 儲存副作用函式 const bucket = new WeakMap()

// 用於儲存被註冊的副作用函式 let activeEffect = null

// 用於接收並註冊副作用函式 function effect(fn) { // 儲存 fn activeEffect = fn

// 執行 fn 函式,目的是初始化執行和觸發 get 攔截 fn() }

// 響應式資料 function reactive(target) { return new Proxy(target, { get(target, key) { console.log("get =", key, Reflect.get(target, key));

  // 沒有註冊副作用函式,直接返回資料
  if (!activeEffect) return Reflect.get(target, key)

  track(target, key)

  return Reflect.get(target, key)
},
set(target, key, newVal) {
  console.log("set ", key, newVal);
  target[key] = newVal

  trigger(target, key)

  return Reflect.set(target, key, newVal)
}

}) }

// 收集依賴 function track(target, key) { // 從 bucket 獲取 depsMap 的依賴關係 let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) }

// 從 depsMap 獲取 deps 集合 let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) }

// 觸發依賴 function trigger(target, key) { // 獲取對應的 depsMap const depsMap = bucket.get(target) if (!depsMap) return

// 獲取對應的 deps const deps = depsMap.get(key) // 執行相應的 effect deps && deps.forEach(effect => effect()) } ```

動態清除無用副作用函式

存在缺陷

若執行下面的測試程式碼,那麼會產生 遺留的副作用函式依賴: ```js // 獲得響應式資料 const data = reactive({ text: 'hello world...', ok: true })

// 註冊副作用函式 effect(() => { console.log('effect running ...') document.body.innerText = data.ok ? data.text : 'not ok' }) `` - 當初始化ok = true時執行,會產生依賴關係為: <img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5ba9b6d96948451abeb897455b787179~tplv-k3u1fbpfcp-watermark.image?" width="100%"> - 當發生修改操作ok = false後,此時會執行對應副作用函式,同時意味著data.text欄位將不會再被訪問到,理想情況是此時data.text` 欄位所對應的副作用函式依賴應該要被清除

完善思路

  • 每次副作用函式執行時,將副作用函式從所有與之有關聯的依賴集合中進行刪除
  • 當副作用函式執行完畢後,又會產生新的依賴關係,但這個新的依賴關係就不會包含遺留的副作用函式

具體程式碼實現

```js // 儲存副作用函式 const bucket = new WeakMap()

// 用於儲存被註冊的副作用函式 let activeEffect = null

// 用於接收並註冊副作用函式 function effect(fn) {

const effectFn = () => { // 先呼叫 cleanup 函式完成舊依賴的清除工作 cleanup(effectFn) // 儲存 fn activeEffect = effectFn // 執行 fn 函式,目的是初始化執行和觸發 get 攔截 fn() }

// 用於儲存所有與其關聯的副作用函式的依賴集合 effectFn.deps = [] // 執行副作用函式 effectFn() }

// 清除本次依賴相關的舊副作用函式 function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i] deps.delete(effectFn) } // 重置 effectFn.deps 陣列 effectFn.deps.length = 0 }

// 響應式資料 function reactive(target) { return new Proxy(target, { get(target, key) {

  // 沒有註冊副作用函式,直接返回資料
  if (!activeEffect) return Reflect.get(target, key)

  track(target, key)

  return Reflect.get(target, key)
},
set(target, key, newVal) {
  target[key] = newVal

  trigger(target, key)

  return Reflect.set(target, key, newVal)
}

}) }

// 收集依賴 function track(target, key) { // 從 bucket 獲取 depsMap 的依賴關係 let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) }

// 從 depsMap 獲取 deps 集合 let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect)

// 將與當前副作用函式存在聯絡的依賴集合 deps 新增到 activeEffect.deps 陣列中 activeEffect.deps.push(deps) }

// 觸發依賴 function trigger(target, key) { // 獲取對應的 depsMap const depsMap = bucket.get(target) if (!depsMap) return // 獲取對應的 deps const effects = depsMap.get(key)

// 構建新的 Set 避免遞迴 const effectsToRun = new Set(effects) // 執行相應的 effect effectsToRun.forEach(effectFn => effectFn()) } ``` ## 支援巢狀的 effect 函式 ### 為什麼要支援巢狀 effect 函式? 用 Vuejs 來舉例,如元件的巢狀就需要支援巢狀的 effect 函式,虛擬碼如下:

存在缺陷

假設存在如下的巢狀關係,存在的缺陷如下: - 當定時器執行並只更改 data.text 的值,此時只有 effect2 執行了,而期望的 effect1 卻沒執行執行 - 原因是 目前使用全域性變數 activeEffect 來儲存通過 effect 函式註冊的副作用函式,意味著同一時刻 activeEffect 儲存的副作用函式只能有一個。當副作用函式發生巢狀時,內層的副作用函式的執行會覆蓋 activeEffect 的值,當外部響應式資料進行依賴收集時,它們收集到的副作用函式將會是內層的副作用函式

image.png

```js // 獲得響應式資料 const data = reactive({ text: 'hello world...', ok: true })

// 註冊副作用函式 effect(() => { effect(() => { console.log('effect2 執行:', data.ok) })

console.log('effect1 執行:', data.text) })

console.log("bucket = ", bucket);

setTimeout(() => { console.log('setTimeout 執行,修改 data.text 的值') data.text = 'hello vue3...' }, 1000); ```

完善思路

通過副作用函式棧 effectStack 將正在執行的副作用函式入棧,等到副作用函式執行完畢後再彈出棧,並保證 activeEffect 始終是指向棧頂的副作用函式。

具體程式碼實現

```js // 儲存副作用函式 const bucket = new WeakMap()

// 用於儲存被註冊的副作用函式 let activeEffect = null

// effect 棧 const effectStack = []

// 用於接收並註冊副作用函式 function effect(fn) {

const effectFn = () => { // 先呼叫 cleanup 函式完成舊依賴的清除工作 cleanup(effectFn) // 儲存 fn activeEffect = effectFn // 在副作用函式呼叫前,將副作用函式入棧 effectStack.push(effectFn) // 執行 fn 函式,目的是初始化執行和觸發 get 攔截 fn() // 副作用函式執行完成後出棧 effectStack.pop() // 將 activeEffect 指向棧頂(原先)的副作用函式 activeEffect = effectStack[effectStack.length - 1] }

// 用於儲存所有與其關聯的副作用函式的依賴集合 effectFn.deps = [] // 執行副作用函式 effectFn() }

// 清除本次依賴相關的舊副作用函式 function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i] deps.delete(effectFn) } // 重置 effectFn.deps 陣列 effectFn.deps.length = 0 }

// 響應式資料 function reactive(target) { return new Proxy(target, { get(target, key) {

  // 沒有註冊副作用函式,直接返回資料
  if (!activeEffect) return Reflect.get(target, key)

  track(target, key)

  return Reflect.get(target, key)
},
set(target, key, newVal) {
  target[key] = newVal

  trigger(target, key)

  return Reflect.set(target, key, newVal)
}

}) }

// 收集依賴 function track(target, key) { // 從 bucket 獲取 depsMap 的依賴關係 let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) }

// 從 depsMap 獲取 deps 集合 let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect)

// 將與當前副作用函式存在聯絡的依賴集合 deps 新增到 activeEffect.deps 陣列中 activeEffect.deps.push(deps) }

// 觸發依賴 function trigger(target, key) { // 獲取對應的 depsMap const depsMap = bucket.get(target) if (!depsMap) return // 獲取對應的 deps const effects = depsMap.get(key)

// 構建新的 Set 避免遞迴 const effectsToRun = new Set(effects) // 執行相應的 effect effectsToRun.forEach(effectFn => effectFn()) } ## 避免無限遞迴迴圈 ### 存在缺陷 如下面的例子,就會產生無限迴圈:js // 獲得響應式資料 const data = reactive({ count: 1 })

// 註冊副作用函式 effect(() => { data.count++ // 等價於 data.count = data.count + 1 }) 其中,既會讀取 `data.count` 的值,又會設定 `data.count` 的值,每次 **trigger** 操作觸發時,本次還沒有執行完,又觸發了下一次的 **trigger** 操作,這就會產生無限遞迴呼叫自身,導致棧溢位。 ### 完善思路 在 **trigger** 操作發生時新增是否執行副作用函式的條件:**若 trigger 觸發執行的副作用函式與當前的正則執行的副作用函式相同,則不觸發執行**。 ### 具體程式碼實現js // 儲存副作用函式 const bucket = new WeakMap()

// 用於儲存被註冊的副作用函式 let activeEffect = null

// effect 棧 const effectStack = []

// 用於接收並註冊副作用函式 function effect(fn) {

const effectFn = () => { // 先呼叫 cleanup 函式完成舊依賴的清除工作 cleanup(effectFn) // 儲存 fn activeEffect = effectFn // 在副作用函式呼叫前,將副作用函式入棧 effectStack.push(effectFn) // 執行 fn 函式,目的是初始化執行和觸發 get 攔截 fn() // 副作用函式執行完成後出棧 effectStack.pop() // 將 activeEffect 指向棧頂(原先)的副作用函式 activeEffect = effectStack[effectStack.length - 1] }

// 用於儲存所有與其關聯的副作用函式的依賴集合 effectFn.deps = [] // 執行副作用函式 effectFn() }

// 清除本次依賴相關的舊副作用函式 function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i] deps.delete(effectFn) } // 重置 effectFn.deps 陣列 effectFn.deps.length = 0 }

// 響應式資料 function reactive(target) { return new Proxy(target, { get(target, key) {

  // 沒有註冊副作用函式,直接返回資料
  if (!activeEffect) return Reflect.get(target, key)

  track(target, key)

  return Reflect.get(target, key)
},
set(target, key, newVal) {
  target[key] = newVal

  trigger(target, key)

  return Reflect.set(target, key, newVal)
}

}) }

// 收集依賴 function track(target, key) { // 從 bucket 獲取 depsMap 的依賴關係 let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) }

// 從 depsMap 獲取 deps 集合 let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect)

// 將與當前副作用函式存在聯絡的依賴集合 deps 新增到 activeEffect.deps 陣列中 activeEffect.deps.push(deps) }

// 觸發依賴 function trigger(target, key) { // 獲取對應的 depsMap const depsMap = bucket.get(target) if (!depsMap) return // 獲取對應的 deps const effects = depsMap.get(key)

// 構建新的 Set 避免遞迴 const effectsToRun = new Set(effects) // 執行相應的 effect effectsToRun && effectsToRun.forEach(effectFn => { // 避免遞迴呼叫自身 if (effectFn !== activeEffect) effectFn() }) } ``` ## 實現可排程執行 — 排程器函式 ### 什麼是可排程性? 可排程指的是當 trigger 觸發副作用函式重新執行時,提供給使用者決定副作用函式執行的時機、次數和方式。

通過下面的程式碼舉個栗子: ```js // 獲得響應式資料 const data = reactive({ count: 1 })

// 註冊副作用函式 effect(() => { console.log(data.count) })

data.count++

console.log('結束了') 其對應的資料輸出結果為:**1 2 '結束了'**,假設使用者需要的輸出順序是:**1 '結束了' 2**,那麼就需要當前的響應式系統支援 **排程**。 ### 實現思路 - 給現有的 **effect** 函式多新增一個可選引數 **options**,允許使用者指定排程器,例如:js effect(()=>{ console.log(data.count) }, { // 將排程器設定名為 scheduler 的函式 scheduler(fn){ ... } }) - 在 **effect** 函式中註冊副作用函式時,將這個 **options** 選項掛載到對應的副作用函式上 - 在 **trigger** 函式觸發副作用函式重新執行時,通過直接呼叫 **options** 中傳入的排程器函式,把控制權移交給使用者 ### 具體程式碼實現js // 儲存副作用函式 const bucket = new WeakMap()

// 用於儲存被註冊的副作用函式 let activeEffect = null

// effect 棧 const effectStack = []

// 用於接收並註冊副作用函式 function effect(fn, options = {}) {

const effectFn = () => { // 先呼叫 cleanup 函式完成舊依賴的清除工作 cleanup(effectFn) // 儲存 fn activeEffect = effectFn // 在副作用函式呼叫前,將副作用函式入棧 effectStack.push(effectFn) // 執行 fn 函式,目的是初始化執行和觸發 get 攔截 fn() // 副作用函式執行完成後出棧 effectStack.pop() // 將 activeEffect 指向棧頂(原先)的副作用函式 activeEffect = effectStack[effectStack.length - 1] }

// 將 options 掛載到 effectFn 上 effectFn.options = options // 用於儲存所有與其關聯的副作用函式的依賴集合 effectFn.deps = [] // 執行副作用函式 effectFn() }

// 清除本次依賴相關的舊副作用函式 function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i] deps.delete(effectFn) } // 重置 effectFn.deps 陣列 effectFn.deps.length = 0 }

// 響應式資料 function reactive(target) { return new Proxy(target, { get(target, key) {

  // 沒有註冊副作用函式,直接返回資料
  if (!activeEffect) return Reflect.get(target, key)

  track(target, key)

  return Reflect.get(target, key)
},
set(target, key, newVal) {
  target[key] = newVal

  trigger(target, key)

  return Reflect.set(target, key, newVal)
}

}) }

// 收集依賴 function track(target, key) { // 從 bucket 獲取 depsMap 的依賴關係 let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) }

// 從 depsMap 獲取 deps 集合 let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect)

// 將與當前副作用函式存在聯絡的依賴集合 deps 新增到 activeEffect.deps 陣列中 activeEffect.deps.push(deps) }

// 觸發依賴 function trigger(target, key) { // 獲取對應的 depsMap const depsMap = bucket.get(target) if (!depsMap) return // 獲取對應的 deps const effects = depsMap.get(key)

// 構建新的 Set 避免遞迴 const effectsToRun = new Set() effects && effects.forEach(effectFn => { // 避免遞迴呼叫自身 if (effectFn !== activeEffect) effectsToRun.add(effectFn) })

// 是否執行排程器函式 effectsToRun.forEach(effectFn => { // 若副作用函式存在排程器,則呼叫排程器,並將 effectFn 函式作為引數傳遞 if (effectFn.options.scheduler) { effectFn.options.scheduler(effectFn) } else { // 否則直接執行副作用函式 effectFn() } }) } ## 基於排程器控制執行次數 ### 為什麼需要控制執行次數? 直接通過如下栗子進行解釋:js // 獲得響應式資料 const data = reactive({ count: 1 })

// 註冊副作用函式 effect(() => { console.log(data.count); })

data.count++ data.count++ ``` 在沒有指定排程器時,以上程式碼執行後輸出結果為:1 2 3,但假設其中的 2 只是個過渡階段,使用者只關心最後的結果 3,那麼執行三次列印操作就是多餘的,即期望輸出為:1 3

實現思路

  • 定義一個任務佇列 jobQueue,選擇 Set 資料結構,目的是利用它的自動去重功能
  • 每次排程執行時,先將當前副作用函式新增到 jobQueue 佇列中
  • 定義一個 flushJob 函式重新整理 jobQueue 佇列中的副作用函式
    • 其中需要設定一個 isFlushing 表示正在重新整理的標誌,用於去判斷是否需要執行,只有當 isFlushing = false 時才需要執行,保證 flushJob 函式在一個週期內只調用一次
    • 最後通過 promise.then 來將重新整理 jobQueue 佇列的執行新增到微任務佇列中

即支援通過如下方式進行呼叫: ```js // 獲得響應式資料 const data = reactive({ count: 1 })

// 註冊副作用函式 effect(() => { console.log(data.count); }, { scheduler(effectFn) { // 將副作用函式新增到 jobQueue 佇列中 jobQueue.add(effectFn) // 呼叫 flushJob 重新整理佇列,減少不必要的執行 flushJob() } })

data.count++ data.count++ ```

具體程式碼實現

```js // 儲存副作用函式 const bucket = new WeakMap()

// 用於儲存被註冊的副作用函式 let activeEffect = null

// effect 棧 const effectStack = []

// 定義 jobQueue 任務佇列 const jobQueue = new Set() // 通過 promise 微任務實現非同步執行 const resolvedPromise = Promise.resolve() function nextTick(fn) { return fn ? resolvedPromise.then(fn) : resolvedPromise } // 表示當前是否正在重新整理佇列 let isFlushing = false

// 重新整理佇列函式 function flushJob() { // 當前正在重新整理佇列,則直接結束 if (isFlushing) return

// 一旦需要執行重新整理佇列,先將 isFlushing 置為 false isFlushing = true

// 在微任務佇列中重新整理 jobQueue 佇列 nextTick(() => { jobQueue.forEach(job => job()) }).finally(() => { // 重新整理佇列結束後,重置 isFlushing isFlushing = false }) }

// 用於接收並註冊副作用函式 function effect(fn, options = {}) {

const effectFn = () => { // 先呼叫 cleanup 函式完成舊依賴的清除工作 cleanup(effectFn) // 儲存 fn activeEffect = effectFn // 在副作用函式呼叫前,將副作用函式入棧 effectStack.push(effectFn) // 執行 fn 函式,目的是初始化執行和觸發 get 攔截 fn() // 副作用函式執行完成後出棧 effectStack.pop() // 將 activeEffect 指向棧頂(原先)的副作用函式 activeEffect = effectStack[effectStack.length - 1] }

// 將 options 掛載到 effectFn 上 effectFn.options = options // 用於儲存所有與其關聯的副作用函式的依賴集合 effectFn.deps = [] // 執行副作用函式 effectFn() }

// 清除本次依賴相關的舊副作用函式 function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i] deps.delete(effectFn) } // 重置 effectFn.deps 陣列 effectFn.deps.length = 0 }

// 響應式資料 function reactive(target) { return new Proxy(target, { get(target, key) {

  // 沒有註冊副作用函式,直接返回資料
  if (!activeEffect) return Reflect.get(target, key)

  track(target, key)

  return Reflect.get(target, key)
},
set(target, key, newVal) {
  target[key] = newVal

  trigger(target, key)

  return Reflect.set(target, key, newVal)
}

}) }

// 收集依賴 function track(target, key) { // 從 bucket 獲取 depsMap 的依賴關係 let depsMap = bucket.get(target) if (!depsMap) { bucket.set(target, (depsMap = new Map())) }

// 從 depsMap 獲取 deps 集合 let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect)

// 將與當前副作用函式存在聯絡的依賴集合 deps 新增到 activeEffect.deps 陣列中 activeEffect.deps.push(deps) }

// 觸發依賴 function trigger(target, key) { // 獲取對應的 depsMap const depsMap = bucket.get(target) if (!depsMap) return // 獲取對應的 deps const effects = depsMap.get(key)

// 構建新的 Set 避免遞迴 const effectsToRun = new Set() effects && effects.forEach(effectFn => { // 避免遞迴呼叫自身 if (effectFn !== activeEffect) effectsToRun.add(effectFn) })

// 是否執行排程器函式 effectsToRun.forEach(effectFn => { // 若副作用函式存在排程器,則呼叫排程器,並將 effectFn 函式作為引數傳遞 if (effectFn.options.scheduler) { effectFn.options.scheduler(effectFn) } else { // 否則直接執行副作用函式 effectFn() } }) } ```

最後

以上內容都是 Vue.js 內部響應式系統的實現思路,但其內部擁有一個更完善處理機制和邊界情況,作為學習者而言瞭解其設計思路和設計原因其實也足夠了,不過作為 coder 不要總是省略動手的過程。