首先,我們先簡單回顧一下:
響應式系統的核心就是 WeakMap --- Map --- Set 的資料結構。
WeakMap 的 key 是原始對象,value 是響應式的 Map。這樣當物件銷毀的時候,對應的 Map 也會銷毀。
Map 的 key 是物件的每個屬性,value 是依賴這個物件屬性的 effect 函數的集合 Set。然後用 Proxy 代理物件的 get 方法,收集依賴該物件屬性的 effect 函數到對應 key 的 Set 中。也要代理物件的 set 方法,修改物件屬性的時候呼叫所有該 key 的 effect 函數。
上篇文章我們依照這樣的想法實作了一個比較完善的響應式系統,然後今天繼續實作 computed。
首先,我們把之前的程式碼重構一下,把依賴收集和觸發依賴函數的執行抽離成track 和trigger 函數:
邏輯還是加入effect 到對應的Set,以及觸發對應Set 裡的effect 函數執行,但抽離出來清晰多了。
然後繼續實作 computed。
computed 的使用大概是這樣的:
const value = computed(() => { return obj.a + obj.b; });
對比下effect:
effect(() => { console.log(obj.a); });
區別只是多了個返回值。
所以我們基於effect 實作computed 就是這樣的:
function computed(fn) { const value = effect(fn); return value }
當然,現在的effect 是沒有回傳值的,要給它加一下:
只是在之前執行effect 函數的基礎上把回傳值記錄下來返回,這個改造還是很容易的。
現在computed 就能回傳計算後的值了:
#但是現在資料一遍,所有的effect 都執行了,而像computed 這裡的effect 是沒必要每次都重新執行的,只需要在資料變了之後執行。
所以我們加上一個 lazy 的 option 來控制 effect 不立刻執行,而是把函數回傳讓使用者自己執行。
然後 computed 裡用 effect 的時候就加入一個 lazy 的 option,讓 effect 函數不執行,而是回傳出來。
computed 裡建立一個對象,在value 的get 觸發時呼叫函數拿到最新的值:
我們測試下:
可以看到現在computed 回傳值的value 屬性是能拿到計算後的值的,並且修改了obj.a.之後會重新執行計算函數,再拿value 時能拿到新的值。
只是多執行了一次計算,這是因為obj.a 變的時候會執行所有的effect 函數:
這樣每次資料變了都會重新執行computed 的函數來計算最新的值。
這是不必要的,effect 的函數是否執行應該也是可以控制的。所以我們要給它加上調度的功能:
可以支援傳入schduler 回呼函數,然後執行effect 的時候,如果有scheduler 就傳給它讓使用者自己來調度,否則才執行effect 函數。
這樣使用者就可以自己控制effect 函數的執行了:
#然後再試一次剛才的程式碼:
可以看到,obj.a 改變了之後並沒有執行effect 函數來重新計算,因為我們加了sheduler 來自己調度。這樣就避免了資料變了以後馬上執行 computed 函數,可以自己控制執行。
現在還有一個問題,每次造訪 res.value 都要計算:
#能不能加个缓存呢?只有数据变了才需要计算,否则直接拿之前计算的值。
当然是可以的,加个标记就行:
scheduler 被调用的时候就说明数据变了,这时候 dirty 设置为 true,然后取 value 的时候就重新计算,之后再改为 false,下次取 value 就直接拿计算好的值了。
我们测试下:
我们访问 computed 值的 value 属性时,第一次会重新计算,后面就直接拿计算好的值了。
修改它依赖的数据后,再次访问 value 属性会再次重新计算,然后后面再访问就又会直接拿计算好的值了。
至此,我们完成了 computed 的功能。
但现在的 computed 实现还有一个问题,比如这样一段代码:
let res = computed(() => { return obj.a + obj.b; }); effect(() => { console.log(res.value); });
我们在一个 effect 函数里用到了 computed 值,按理说 obj.a 变了,那 computed 的值也会变,应该触发所有的 effect 函数。
但实际上并没有:
这是为什么呢?
这是因为返回的 computed 值并不是一个响应式的对象,需要把它变为响应式的,也就是 get 的时候 track 收集依赖,set 的时候触发依赖的执行:
我们再试一下:
现在 computed 值变了就能触发依赖它的 effect 了。至此,我们的 computed 就很完善了。
完整代码如下:
const data = { a: 1, b: 2 } let activeEffect const effectStack = []; function effect(fn, options = {}) { const effectFn = () => { cleanup(effectFn) activeEffect = effectFn effectStack.push(effectFn); const res = fn() effectStack.pop() activeEffect = effectStack[effectStack.length - 1] return res } effectFn.deps = [] effectFn.options = options; if (!options.lazy) { effectFn() } return effectFn } function computed(fn) { let value let dirty = true const effectFn = effect(fn, { lazy: true, scheduler(fn) { if(!dirty) { dirty = true trigger(obj, 'value'); } } }); const obj = { get value() { if (dirty) { value = effectFn() dirty = false } track(obj, 'value'); console.log(obj); return value } } return obj } function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i] deps.delete(effectFn) } effectFn.deps.length = 0 } const reactiveMap = new WeakMap() const obj = new Proxy(data, { get(targetObj, key) { track(targetObj, key); return targetObj[key] }, set(targetObj, key, newVal) { targetObj[key] = newVal trigger(targetObj, key) } }) function track(targetObj, key) { let depsMap = reactiveMap.get(targetObj) if (!depsMap) { reactiveMap.set(targetObj, (depsMap = new Map())) } let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } deps.add(activeEffect) activeEffect.deps.push(deps); } function trigger(targetObj, key) { const depsMap = reactiveMap.get(targetObj) if (!depsMap) return const effects = depsMap.get(key) const effectsToRun = new Set(effects) effectsToRun.forEach(effectFn => { if(effectFn.options.scheduler) { effectFn.options.scheduler(effectFn) } else { effectFn() } }) }
以上是Vue3響應式系統怎麼實現computed的詳細內容。更多資訊請關注PHP中文網其他相關文章!