Vue3?響應(yīng)式系統(tǒng)實(shí)現(xiàn)?computed
前言
上篇文章我們實(shí)現(xiàn)了基本的響應(yīng)式系統(tǒng),這篇文章繼續(xù)實(shí)現(xiàn) computed。
首先,我們簡(jiǎn)單回顧一下:
響應(yīng)式系統(tǒng)的核心就是一個(gè) WeakMap --- Map --- Set 的數(shù)據(jù)結(jié)構(gòu)。
WeakMap 的 key 是原對(duì)象,value 是響應(yīng)式的 Map。這樣當(dāng)對(duì)象銷毀的時(shí)候,對(duì)應(yīng)的 Map 也會(huì)銷毀。
Map 的 key 就是對(duì)象的每個(gè)屬性,value 是依賴這個(gè)對(duì)象屬性的 effect 函數(shù)的集合 Set。然后用 Proxy 代理對(duì)象的 get 方法,收集依賴該對(duì)象屬性的 effect 函數(shù)到對(duì)應(yīng) key 的 Set 中。還要代理對(duì)象的 set 方法,修改對(duì)象屬性的時(shí)候調(diào)用所有該 key 的 effect 函數(shù)。
上篇文章我們按照這樣的思路實(shí)現(xiàn)了一個(gè)比較完善的響應(yīng)式系統(tǒng),然后今天繼續(xù)實(shí)現(xiàn) computed。
實(shí)現(xiàn) computed
首先,我們把之前的代碼重構(gòu)一下,把依賴收集和觸發(fā)依賴函數(shù)的執(zhí)行抽離成 track 和 trigger 函數(shù):
邏輯還是添加 effect 到對(duì)應(yīng)的 Set,以及觸發(fā)對(duì)應(yīng) Set 里的 effect 函數(shù)執(zhí)行,但抽離出來(lái)清晰多了。
然后繼續(xù)實(shí)現(xiàn) computed。
computed 的使用大概是這樣的:
const value = computed(() => { return obj.a + obj.b; });
對(duì)比下 effect:
effect(() => { console.log(obj.a); });
區(qū)別只是多了個(gè)返回值。
所以我們基于 effect 實(shí)現(xiàn) computed 就是這樣的:
function computed(fn) { const value = effect(fn); return value }
當(dāng)然,現(xiàn)在的 effect 是沒(méi)有返回值的,要給它加一下:
只是在之前執(zhí)行 effect 函數(shù)的基礎(chǔ)上把返回值記錄下來(lái)返回,這個(gè)改造還是很容易的。
現(xiàn)在 computed 就能返回計(jì)算后的值了:
但是現(xiàn)在數(shù)據(jù)一遍,所有的 effect 都執(zhí)行了,而像 computed 這里的 effect 是沒(méi)必要每次都重新執(zhí)行的,只需要在數(shù)據(jù)變了之后執(zhí)行。
所以我們添加一個(gè) lazy 的 option 來(lái)控制 effect 不立刻執(zhí)行,而是把函數(shù)返回讓用戶自己執(zhí)行。
然后 computed 里用 effect 的時(shí)候就添加一個(gè) lazy 的 option,讓 effect 函數(shù)不執(zhí)行,而是返回出來(lái)。
computed 里創(chuàng)建一個(gè)對(duì)象,在 value 的 get 觸發(fā)時(shí)調(diào)用該函數(shù)拿到最新的值:
我們測(cè)試下:
可以看到現(xiàn)在 computed 返回值的 value 屬性是能拿到計(jì)算后的值的,并且修改了 obj.a. 之后會(huì)重新執(zhí)行計(jì)算函數(shù),再次拿 value 時(shí)能拿到新的值。
只是多執(zhí)行了一次計(jì)算,這是因?yàn)?obj.a 變的時(shí)候會(huì)執(zhí)行所有的 effect 函數(shù):
這樣每次數(shù)據(jù)變了都會(huì)重新執(zhí)行 computed 的函數(shù)來(lái)計(jì)算最新的值。
這是沒(méi)有必要的,effect 的函數(shù)是否執(zhí)行應(yīng)該也是可以控制的。所以我們要給它加上調(diào)度的功能:
可以支持傳入 schduler 回調(diào)函數(shù),然后執(zhí)行 effect 的時(shí)候,如果有 scheduler 就傳給它讓用戶自己來(lái)調(diào)度,否則才執(zhí)行 effect 函數(shù)。
這樣用戶就可以自己控制 effect 函數(shù)的執(zhí)行了:
然后再試一下剛才的代碼:
可以看到,obj.a 變了之后并沒(méi)有執(zhí)行 effect 函數(shù)來(lái)重新計(jì)算,因?yàn)槲覀兗恿?sheduler 來(lái)自己調(diào)度。這樣就避免了數(shù)據(jù)變了以后馬上執(zhí)行 computed 函數(shù),可以自己控制執(zhí)行。
現(xiàn)在還有一個(gè)問(wèn)題,每次訪問(wèn) res.value 都要計(jì)算:
能不能加個(gè)緩存呢?只有數(shù)據(jù)變了才需要計(jì)算,否則直接拿之前計(jì)算的值。
當(dāng)然是可以的,加個(gè)標(biāo)記就行:
scheduler 被調(diào)用的時(shí)候就說(shuō)明數(shù)據(jù)變了,這時(shí)候 dirty 設(shè)置為 true,然后取 value 的時(shí)候就重新計(jì)算,之后再改為 false,下次取 value 就直接拿計(jì)算好的值了。
我們測(cè)試下:
我們?cè)L問(wèn) computed 值的 value 屬性時(shí),第一次會(huì)重新計(jì)算,后面就直接拿計(jì)算好的值了。
修改它依賴的數(shù)據(jù)后,再次訪問(wèn) value 屬性會(huì)再次重新計(jì)算,然后后面再訪問(wèn)就又會(huì)直接拿計(jì)算好的值了。
至此,我們完成了 computed 的功能。
但現(xiàn)在的 computed 實(shí)現(xiàn)還有一個(gè)問(wèn)題,比如這樣一段代碼:
let res = computed(() => { return obj.a + obj.b; }); effect(() => { console.log(res.value); });
我們?cè)谝粋€(gè) effect 函數(shù)里用到了 computed 值,按理說(shuō) obj.a 變了,那 computed 的值也會(huì)變,應(yīng)該觸發(fā)所有的 effect 函數(shù)。
但實(shí)際上并沒(méi)有:
這是為什么呢?
這是因?yàn)榉祷氐?computed 值并不是一個(gè)響應(yīng)式的對(duì)象,需要把它變?yōu)轫憫?yīng)式的,也就是 get 的時(shí)候 track 收集依賴,set 的時(shí)候觸發(fā)依賴的執(zhí)行:
我們?cè)僭囈幌拢?/strong>
現(xiàn)在 computed 值變了就能觸發(fā)依賴它的 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() } }) }
總結(jié)
上篇文章我們實(shí)現(xiàn)了響應(yīng)式的核心數(shù)據(jù)結(jié)構(gòu),依賴的收集、數(shù)據(jù)變化后通知依賴函數(shù)執(zhí)行。今天我們?cè)谀腔A(chǔ)上實(shí)現(xiàn)了 computed。我們改造了 effect 函數(shù),讓它返回傳入的 fn,然后在 computed 里自己執(zhí)行來(lái)拿到計(jì)算后的值。
我們又支持了 lazy 和 scheduler 的 option,lazy 是讓 effect 不立刻執(zhí)行傳入的函數(shù),scheduler 是在數(shù)據(jù)變動(dòng)觸發(fā)依賴執(zhí)行的時(shí)候回調(diào) sheduler 來(lái)調(diào)度。
我們通過(guò)標(biāo)記是否 dirty 來(lái)實(shí)現(xiàn)緩存,當(dāng) sheduler 執(zhí)行的時(shí)候,說(shuō)明數(shù)據(jù)變了,把 dirty 置為 true,重新計(jì)算 computed 的值,否則直接拿緩存。此外,computed 的 value 并不是響應(yīng)式對(duì)象,我們需要單獨(dú)的調(diào)用下 track 和 trigger。這樣,我們就實(shí)現(xiàn)了完善的 computed 功能,vue3 內(nèi)部也是這樣實(shí)現(xiàn)的。
到此這篇關(guān)于 Vue3 響應(yīng)式系統(tǒng)實(shí)現(xiàn) computed的文章就介紹到這了,更多相關(guān) Vue3 computed內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue引用vee-validate插件表單驗(yàn)證問(wèn)題(cdn方式引用)
這篇文章主要介紹了Vue引用vee-validate插件表單驗(yàn)證問(wèn)題(cdn方式引用),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12基于vue3+antDesign2+echarts?實(shí)現(xiàn)雷達(dá)圖效果
這篇文章主要介紹了基于vue3+antDesign2+echarts?實(shí)現(xiàn)雷達(dá)圖,本文通過(guò)實(shí)例代碼圖文相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08Vue動(dòng)態(tài)樣式方法實(shí)例總結(jié)
在vue項(xiàng)目中,很多場(chǎng)景要求我們動(dòng)態(tài)改變?cè)氐臉邮?下面這篇文章主要給大家介紹了關(guān)于Vue動(dòng)態(tài)樣式方法的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02Vue.js與 ASP.NET Core 服務(wù)端渲染功能整合
本文通過(guò)案例給大家詳細(xì)分析了ASP.NET Core 與 Vue.js 服務(wù)端渲染,需要的朋友可以參考下2017-11-11Vue3中關(guān)于getCurrentInstance的大坑及解決
這篇文章主要介紹了Vue3中關(guān)于getCurrentInstance的大坑及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04vue復(fù)合組件實(shí)現(xiàn)注冊(cè)表單功能
這篇文章主要為大家詳細(xì)介紹了vue復(fù)合組件實(shí)現(xiàn)注冊(cè)表單功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11vite?+?electron-builder?打包配置詳解
這篇文章主要為大家介紹了electron基于vite?+?electron-builder?打包配置詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09