在之前的文章中,我們已經完成了 ref 實作,它能將原始值包裝成響應式物件。現在,我們要接續完成另一部分的響應式系統核心:reactive 函式。我們的目標是接收一個完整的物件,並回傳一個代理物件,使其所有屬性都具備響應性。
目標設定
我們的目標很明確:完成一個 reactive 函式,讓行為跟 Vue 的官方範例一樣。
環境建置
1// import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'2import { reactive, effect } from '../dist/reactivity.esm.js'3
4const state = reactive({5 a: 06})7effect(() => {8 console.log(state.a)9})10
11setTimeout(() => {12 state.a = 113}, 1000)
我們期待初始化頁面輸出 0,一秒鐘後輸出1。
用註解的官方的範例,我們很明顯看到輸出值。
我們先在src
底下新增一個 reactive.ts
1export function reactive(target){2}
並且在 index.ts
引入
1export * from './ref'2export * from './effect'3export * from './reactive'
另外我們在 shared/src/index.ts
存放工具函式這邊寫一個物件判斷函式。
1export function isObject(value) {2 return typeof value === 'object' && value !== null3}
核心思路
我們再另外寫一個函式createReactiveObject
,我們實際的邏輯並不在 reactive
函式中。
主要是createReactiveObject
之後其他地方會用到,像是 shallowReactive
之類的。
1export function reactive(target){2 return createReactiveObject(target)3}
接下來思考 createReactiveObject
他本身的限制,以及我們的需求,
- 他只能傳入物件類型,所以我們要去判斷他的型別。
reactive
的核心是用一個Proxy
物件來處理。Proxy
的物件中會需要 get 和 set 處理收集依賴、觸發更新。
- 收集依賴:
target
本身就是依賴,因此我們需要在收集依賴時,把target
跟effect
(也就是sub
)建立關聯關係。 - 觸發更新:通知之前收集的依賴,重新執行。
為什麼 Vue 3 的 reactive()
特別適合使用 Proxy?
主要是因為有幾個特性
Proxy
可以攔截並自定義物件的各種操作,不只是屬性的讀取和設置- 與 Vue 2 使用
Object.defineProperty()
相比,Proxy
的最大優勢是可以偵測新增的屬性 Proxy
可以直接攔截陣列的索引操作和 length 變更Proxy
可以處理Map
、Set
、WeakMap
、WeakSet
等集合類型
看來針對物件類型的 reactive
,Proxy
物件的確是一個更好的解決方案,那我們開始實作!
初步實作 - 借鏡 Ref 實作
1import { isObject } from '@vue/shared'2
3function createReactiveObject(target){4 // reactive 只處理物件5 if(!isObject(target)) return target6
7 // 建立 target 的代理物件8 const proxy = new Proxy(target, {9 get(target, key){10 // 收集依賴:綁定target的屬性與effect的關係11 console.log(target, key)12 return Reflect.get(target, key)13 },14 set(target, key, newValue){15 // 觸發更新:通知之前收集的依賴,重新執行effect7 collapsed lines
16 console.log(target, key, newValue)17 return Reflect.set(target, key, newValue)18 }19 })20
21 return proxy22}
我們來看一下,實際上的輸出值:
看來好像蠻接近的,但依照我們寫 ref
的經驗,我們還需要做鏈表相關邏輯。
先回顧一下我們的 ref 之前怎麼寫的:
1export function trackRef(dep) {2 if (activeSub) {3 link(dep, activeSub)4 }5}6
7export function triggerRef(dep) {8 if (dep.subs) {9 propagate(dep.subs)10 }11}
- get有一個
trackRef
函式,trackRef
函式判斷是不是有effect
(activeSub
),有的話將依賴(dep
)以及effect
(activeSub
)傳入link
函式跟做鏈表關聯關係。 - set有一個
triggerRef
函式,triggerRef
函式判斷是不是收集的依賴有effect
,有的話就傳入propagate
作觸發更新。
看來這個依賴(dep
)很重要,那什麼是依賴?
1class RefImpl {2 _value;3 [ReactiveFlags.IS_REF] = true4
5 subs: Link6 subsTail: Link7 constructor(value) {8 this._value = value9 }10
11 get value() {12 if (activeSub) {13 trackRef(this)14 }15 return this._value7 collapsed lines
16 }17
18 set value(newValue) {19 this._value = newValue20 triggerRef(this)21 }22}
我們可以看到傳入只有
- sub
- subsTail
那我們可以認定只要有這兩個屬性,他就是一個 dep
,那我們可以建立一個 Dep 類別,其他照 ref 的 trackRef 和 triggerRef 邏輯複製過來,並修改。
1import { activeSub } from './effect'2import { link, propagate, Link } from './system'3
4function createReactiveObject(target){5 // reactive 只處理物件6 if(!isObject(target)) return target7
8 // 建立 target 的代理物件9 const proxy = new Proxy(target, {10 get(target, key){11 // 收集依賴:綁定target的屬性與effect的關係12 track(target, key)13 return Reflect.get(target, key)14 },15 set(target, key, newValue){25 collapsed lines
16 // 觸發更新:通知之前收集的依賴,重新執行effect17 trigger(target, key)18 return Reflect.set(target, key, newValue)19 }20 })21
22 return proxy23}24
25class Dep{26 subs: Link27 subsTail: Link28 constructor29}30
31function track(target, key){32 if(!activeSub)return33 link(dep, activeSub) // 有問題34}35
36function trigger(target, key){37 if (dep.subs) {38 propagate(dep.subs) // 有問題39 }40}
這邊有個地方要注意,觸發通知的話要先更新數值,再去通知重新執行,所以 set 這邊要這樣寫:
1set(target, key, newValue){2 const res = Reflect.set(target, key, newValue)3 // 觸發更新:通知之前收集的依賴,重新執行effect4 trigger(target, key)5 return res6}
名稱重複,調整一下 system.ts
interface 名稱。
1interface Dependency {2 subs: Link | undefined3 subsTail: Link | undefined4}5
6export interface Link {7 ...8 dep: Dependency9 ...10}
感覺新建一個Dep
類別的實例,傳進 track
就可以了,不過使用者傳入的 target
物件跟我們的新建的Dep
似乎沒有關係。
看起來我們遇到了一些問題:
- 我們不能再用一個 Dep 來管理所有依賴,必須為物件的每個屬性都維護一個 Dep。
- 如何建立 target.a → Dep for a 的對應關係?
- 如何在不污染原始 target 物件的情況下,儲存 target、key 與 Dep 之間的關聯?
為了解決這個問題,我們需要引入一個更複雜的資料結構來儲存,明天我們再接續探討。