在之前的文章中,我們已經完成了 ref 實作,它能將原始值包裝成響應式物件。現在,我們要接續完成另一部分的響應式系統核心:reactive 函式。我們的目標是接收一個完整的物件,並回傳一個代理物件,使其所有屬性都具備響應性。
我們的目標很明確:完成一個 reactive 函式,讓行為跟 Vue 的官方範例一樣。
// 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
})
effect(() => {
console.log(state.a)
})
setTimeout(() => {
state.a = 1
}, 1000)
我們期待初始化頁面輸出 0,一秒鐘後輸出1。
用註解的官方的範例,我們很明顯看到輸出值。
我們先在src
底下新增一個 reactive.ts
export function reactive(target){
}
並且在 index.ts
引入
export * from './ref'
export * from './effect'
export * from './reactive'
另外我們在 shared/src/index.ts
存放工具函式這邊寫一個物件判斷函式。
export function isObject(value) {
return typeof value === 'object' && value !== null
}
我們再另外寫一個函式createReactiveObject
,我們實際的邏輯並不在 reactive
函式中。
主要是createReactiveObject
之後其他地方會用到,像是 shallowReactive
之類的。
export function reactive(target){
return createReactiveObject(target)
}
接下來思考 createReactiveObject
他本身的限制,以及我們的需求,
reactive
的核心是用一個 Proxy
物件來處理。Proxy
的物件中會需要 get 和 set 處理收集依賴、觸發更新。target
本身就是依賴,因此我們需要在收集依賴時,把 target
跟 effect
(也就是sub
)建立關聯關係。reactive()
特別適合使用 Proxy?主要是因為有幾個特性
Proxy
可以攔截並自定義物件的各種操作,不只是屬性的讀取和設置Object.defineProperty()
相比,Proxy
的最大優勢是可以偵測新增的屬性Proxy
可以直接攔截陣列的索引操作和 length 變更Proxy
可以處理 Map
、Set
、WeakMap
、WeakSet
等集合類型看來針對物件類型的 reactive
,Proxy
物件的確是一個更好的解決方案,那我們開始實作!
import { isObject } from '@vue/shared'
function createReactiveObject(target){
// reactive 只處理物件
if(!isObject(target)) return target
// 建立 target 的代理物件
const proxy = new Proxy(target, {
get(target, key){
// 收集依賴:綁定target的屬性與effect的關係
console.log(target, key)
return Reflect.get(target, key)
},
set(target, key, newValue){
// 觸發更新:通知之前收集的依賴,重新執行effect
console.log(target, key, newValue)
return Reflect.set(target, key, newValue)
}
})
return proxy
}
我們來看一下,實際上的輸出值:
看來好像蠻接近的,但依照我們寫 ref
的經驗,我們還需要做鏈表相關邏輯。
先回顧一下我們的 ref 之前怎麼寫的:
export function trackRef(dep) {
if (activeSub) {
link(dep, activeSub)
}
}
export function triggerRef(dep) {
if (dep.subs) {
propagate(dep.subs)
}
}
trackRef
函式,trackRef
函式判斷是不是有effect
(activeSub
),有的話將依賴(dep
)以及effect
(activeSub
)傳入link
函式跟做鏈表關聯關係。triggerRef
函式,triggerRef
函式判斷是不是收集的依賴有effect
,有的話就傳入propagate
作觸發更新。看來這個依賴(dep
)很重要,那什麼是依賴?
class RefImpl {
_value;
[ReactiveFlags.IS_REF] = true
subs: Link
subsTail: Link
constructor(value) {
this._value = value
}
get value() {
if (activeSub) {
trackRef(this)
}
return this._value
}
set value(newValue) {
this._value = newValue
triggerRef(this)
}
}
我們可以看到傳入只有
那我們可以認定只要有這兩個屬性,他就是一個 dep
,那我們可以建立一個 Dep 類別,其他照 ref 的 trackRef 和 triggerRef 邏輯複製過來,並修改。
import { activeSub } from './effect'
import { link, propagate, Link } from './system'
function createReactiveObject(target){
// reactive 只處理物件
if(!isObject(target)) return target
// 建立 target 的代理物件
const proxy = new Proxy(target, {
get(target, key){
// 收集依賴:綁定target的屬性與effect的關係
track(target, key)
return Reflect.get(target, key)
},
set(target, key, newValue){
// 觸發更新:通知之前收集的依賴,重新執行effect
trigger(target, key)
return Reflect.set(target, key, newValue)
}
})
return proxy
}
class Dep{
subs: Link
subsTail: Link
constructor
}
function track(target, key){
if(!activeSub)return
link(dep, activeSub) // 有問題
}
function trigger(target, key){
if (dep.subs) {
propagate(dep.subs) // 有問題
}
}
這邊有個地方要注意,觸發通知的話要先更新數值,再去通知重新執行,所以 set 這邊要這樣寫:
set(target, key, newValue){
const res = Reflect.set(target, key, newValue)
// 觸發更新:通知之前收集的依賴,重新執行effect
trigger(target, key)
return res
}
名稱重複,調整一下 system.ts
interface 名稱。
interface Dependency {
subs: Link | undefined
subsTail: Link | undefined
}
export interface Link {
...
dep: Dependency
...
}
感覺新建一個Dep
類別的實例,傳進 track
就可以了,不過使用者傳入的 target
物件跟我們的新建的Dep
似乎沒有關係。
看起來我們遇到了一些問題:
為了解決這個問題,我們需要引入一個更複雜的資料結構來儲存,明天我們再接續探討。
同步更新《嘿,日安!》技術部落格