在之前的文章中,我們已經完成了 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 之間的關聯?
為了解決這個問題,我們需要引入一個更複雜的資料結構來儲存,明天我們再接續探討。