iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
Vue.js

從零到一打造 Vue3 響應式系統系列 第 19

Day 19 - Reactive:reactive 的基礎實作

  • 分享至 

  • xImage
  •  

banner
上一次我們提到:

  • 每個物件的每個屬性都需要自己的 Dep。
  • 如何建立 target.a → Dep 的對應關係?
  • 如何在不污染原始物件的情況下儲存這個關係?

我們可以先來做一個簡單的比較

Ref、Reactive 比較

Ref Reactive
資料結構 單一值 物件(多個屬性)
依賴儲存 直接在實例上(this) 需要一個外部的的儲存機制
一個 ref 一個 Dep 多個 Dep(每個屬性一個)

Ref 可以用 this 因為它就是一個實例。

但 Reactive 的每個屬性都需要自己的 Dep,要存在哪?

那我們這時候可以建立一個 Weak Map 物件。

什麼是 Weak Map?

  • WeakMap 是一種 鍵值對集合(key-value pairs)。
  • key 只能是物件(不能是字串、數字、布林),value 可以是任意型別。
  • 弱引用(weak reference):如果一個物件只被 WeakMap 當 key 使用,而程式中沒有其它變數參考它,這個物件就會被垃圾回收(GC)自動清掉。

看來來正好適合我們去做關聯關係。

核心概念

WeakMap 建立一個全域的 targetMap,它的三層巢狀結構如下:

  1. 第一層 targetMap (WeakMap)key 是原始的目標物件 targetvalue 是第二層的 depsMap{ target => depsMap }
  2. 第二層 depsMap (Map)keytarget 物件中的屬性名 keyvalue 是第三層的 dep{ key => dep }
  3. 第三層 dep (Dep 實例) :依賴的容器,儲存了所有訂閱該屬性變更的 effect{ subs, subsTail }

為何不直接用 Map?

因為如果使用一般的 Map,Map 會一直保持對 target 物件的引用,只要它還存在於 Map 中,GC 就無法回收,導致記憶體洩漏。

const targetMap = new WeakMap()

它的結構會長這樣

target = {
  a:0,
  b:1
}

tagetMap = {
  [obj]:{
    a:Dep,
    b:Dep
  }
}

這樣子 Dep 跟 target 就有關係了,一個屬性對應一個 Dep,我們可以通過 target 找到 obj 對應的物件,還可以透過屬性a找到Dep實例,這樣就可以建立關聯關係。

targetMap 的 key 是 obj ,value 是一個 map,這個 map 裡面,map 裡面的 key 就是 obj 的屬性,value 就是對應的 dep,這樣就可以收集依賴。

day19-01

等到需要觸發更新,透過 obj 找到對應的 Map,再透過 key 找到對應的 Dep 通知更新。

收集依賴

收集依賴有分為首次收集依賴,跟之前已經收集過了,所以我們可以這樣寫。

function track(target, key){
  if(!activeSub)return
  // 透過 targetMap 取得 target 的依賴
  let depsMap = targetMap.get(target)

  //首次收集依賴,之前沒有收集過,就新建一個
  // key:obj / value:depsMap
  if(!depsMap){
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  let dep = depsMap.get(key)

  // 收集依賴:第一次建立物件依賴關聯,並且保存到depsMap中
  // key:key / value:Dep
  if(!dep){
    dep = new Dep()
    depsMap.set(key, dep)
  }

  console.log(targetMap, dep)

  link(dep, activeSub)
}

可以 console.log 看起來targetMapdep

day19-02

看起來的確是我們想的那樣,接下來做觸發更新。

觸發更新

觸發更新的話,原本是寫去找依賴 (dep)的 effect(sub),如果找到,就傳入propagate,現在我們的 dep 都存入了depsMap,那我們就理應去depsMap找:

function trigger(target, key){
  const depsMap = targetMap.get(target)
  // 如果 depsMap 不存在,表示沒有收集過依賴,直接返回
  if(!depsMap)return

  const dep = depsMap.get(key)
  // 如果依賴不存在,表示這個 key 沒有在effect中被使用過,直接返回
  if(!dep)return

  // 找到依賴,觸發更新
  propagate(dep.subs)
}

接下來回去看我們的範例,初始化成功輸出0,一秒之後輸出1。

day19-03

看起來成功了,接下來我們來測試 reactivegetter 的響應追蹤:

//import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { reactive, effect } from '../dist/reactivity.esm.js'

const state = reactive({
  a: 0,
  get count(){
    return this.a
  }
})
effect(() => {
  console.log(state.count)
})

setTimeout(() => {
  state.a = 1
}, 1000)

預期結果是先輸出 0,一秒後輸出 1。但實際執行後,我們發現只有初始的 0 被輸出,state.a = 1 的更新並未觸發 effect

透過在 track 函式中輸出 (target, key),發現只有 count 屬性被追蹤了,a 屬性並沒有。

原因在於 return this.a 上,在 getter 內部,this 預設指向的是原始的 target 物件,而不是我們的 proxy 物件。

因此 this.a 的取值過程繞過了 Proxygeta 屬性的依賴自然也沒辦法收集。

const state = reactive({
  a: 0,
  get count() {
    return this.a  // this 應該指向誰?
  }
})

它應該要指向我們的 Proxy 物件而不是原始物件,這樣它在觸發getter的時候,才會執行track(target, key)

那我們要怎麼做?

Proxyhandler 提供第三個參數 receiver,它指向的就是 proxy 物件本身,因此我們只需要將它傳遞給 Reflect.get 就可以修正 this 的指向。

function createReactiveObject(target) {
  // reactive 只處理物件
  if (!isObject(target)) return target

  // 建立 target 的代理物件
  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      // 收集依賴:綁定target的屬性與effect的關係
      track(target, key)
      return Reflect.get(target, key,receiver)
    },
    set(target, key, newValue, receiver) {
      const res = Reflect.set(target, key, newValue, receiver)
      // 觸發更新:通知之前收集的依賴,重新執行effect
      trigger(target, key)
      return res
    }
  })

  return proxy
}

這樣我們就完成,也可以看到初始化 console.log 輸出0,一秒之後輸出1。

執行步驟

day19-04

回顧我們今天:

  • 我們引入了以 WeakMap 為核心的 targetMap 資料結構,解決在不污染原始物件的前提下,為多屬性物件管理各自依賴。
  • 我們實作與 targetMap 配套的 tracktrigger 函式。
  • 我們利用 Proxyreceiver 參數,修正 getterthis 指向的問題。

同步更新《嘿,日安!》技術部落格


上一篇
Day 18 - Reactive:深入 Proxy 的設計思路
下一篇
Day 20 - Reactive:reactive 極端案例
系列文
從零到一打造 Vue3 響應式系統20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言