淺析Proxy如何實(shí)現(xiàn)Vue響應(yīng)式
前言
在面試官:Vue3響應(yīng)式系統(tǒng)都不會(huì)寫(xiě),還敢說(shuō)精通?中我們實(shí)現(xiàn)了一個(gè)最基本的響應(yīng)式系統(tǒng)。
它包含以下功能:
- 借助Proxy將一個(gè)對(duì)象obj變成響應(yīng)式數(shù)據(jù),攔截其get和set操作。
- 通過(guò)effect注冊(cè)副作用函數(shù),并在首次執(zhí)行副作用函數(shù)時(shí)完成obj對(duì)象的依賴收集(track)。
- 當(dāng)數(shù)據(jù)發(fā)生變化的時(shí)候,第2步注冊(cè)的副作用函數(shù)會(huì)重新執(zhí)行(trigger)。
回顧源碼
const?bucket?=?new?WeakMap() //?重新定義bucket數(shù)據(jù)類型為WeakMap let?activeEffect const?effect?=?function?(fn)?{ ??activeEffect?=?fn ??fn() } //?track表示追蹤的意思 function?track?(target,?key)?{ ??//?activeEffect無(wú)值意味著沒(méi)有執(zhí)行effect函數(shù),無(wú)法收集依賴,直接return掉 ??if?(!activeEffect)?{ ????return ??} ??//?每個(gè)target在bucket中都是一個(gè)Map類型:?key?=>?effects ??let?depsMap?=?bucket.get(target) ??//?第一次攔截,depsMap不存在,先創(chuàng)建聯(lián)系 ??if?(!depsMap)?{ ????bucket.set(target,?(depsMap?=?new?Map())) ??} ??//?根據(jù)當(dāng)前讀取的key,嘗試讀取key的effects函數(shù)?? ??let?deps?=?depsMap.get(key) ??if?(!deps)?{ ????//?deps本質(zhì)是個(gè)Set結(jié)構(gòu),即一個(gè)key可以存在多個(gè)effect函數(shù),被多個(gè)effect所依賴 ????depsMap.set(key,?(deps?=?new?Set())) ??} ??//?將激活的effectFn存進(jìn)桶中 ??deps.add(activeEffect) } //?trigger執(zhí)行依賴 function?trigger?(target,?key)?{ ??//?讀取depsMap?其結(jié)構(gòu)是?key?=>?effects ??const?depsMap?=?bucket.get(target) ??if?(!depsMap)?{ ????return ??} ??//?真正讀取依賴當(dāng)前屬性值key的effects ??const?effects?=?depsMap.get(key) ??//?挨個(gè)執(zhí)行即可 ??effects?&&?effects.forEach((fn)?=>?fn()) } //?統(tǒng)一對(duì)外暴露響應(yīng)式函數(shù) function?reactive?(state)?{ ??return?new?Proxy(state,?{ ????get?(target,?key)?{ ??????const?value?=?target[?key?] ??????track(target,?key) ??????//?console.log(`get?${key}:?${value}`) ??????return?value ????}, ????set?(target,?key,?newValue)?{ ??????//?console.log(`set?${key}:?${newValue}`) ??????//?設(shè)置屬性值 ??????target[?key?]?=?newValue ??????trigger(target,?key) ????} ??}) }
測(cè)試一下
const?state?=?reactive({ ??name:?'fatfish', ??age:?100 }) //?effect1 effect(()?=>?{ ??console.log(state.name,?'name') }) //?effect2 effect(()?=>?{ ??console.log(state.age,?'age') }) state.name?=?'fatfish2'?//?因?yàn)閚ame屬性發(fā)生變化了,effect1將會(huì)重新執(zhí)行,打印出的name是fatfish2
看起來(lái)還不錯(cuò),不過(guò)他還存在很多缺陷和不足,比如:
- 分支切換會(huì)導(dǎo)致不必要的effect執(zhí)行損耗
- effect不支持嵌套注冊(cè)副作用函數(shù)
- ...
咱們挨個(gè)看看,這都是些啥...
支持分支切換
什么是分支切換?
按照上文的結(jié)論,這段代碼執(zhí)行后會(huì)形成這樣的數(shù)據(jù)結(jié)構(gòu)。
state
|___ok
|___ effectFn
|___text
|___ effectFn
const?state?=?reactive({ ??ok:?true, ??text:?'hello?world', }); effect(()?=>?{ ??console.log('渲染執(zhí)行') ??document.querySelector('#app').innerHTML?=?state.ok???state.text?:?'not' })
當(dāng)我們把ok
的值改成false
后,頁(yè)面將渲染為"not"。意味著后續(xù)無(wú)論text
如何變化,頁(yè)面都永遠(yuǎn)只可能是"not"。
所以當(dāng)我們修改text
的值時(shí),副作用函數(shù)重新執(zhí)行是沒(méi)有必要的。
const?state?=?reactive({ ??ok:?true, ??text:?'hello?world', }); effect(()?=>?{ ??console.log('渲染執(zhí)行') ??document.querySelector('#app').innerHTML?=?state.ok???state.text?:?'not' }) setTimeout(()?=>?{ ??state.ok?=?false?//?此時(shí)頁(yè)面變成了not ??setTimeout(()?=>?{ ????state.text?=?'other'?//?頁(yè)面依然是not,但是副作用函數(shù)卻還會(huì)執(zhí)行一次 ??},?1000) },?1000)
如何解決?
修改state.text
,副作用函數(shù)會(huì)執(zhí)行是因?yàn)?code>state與其形成的數(shù)據(jù)結(jié)構(gòu)是這樣的。
state
|___ok
|___ effectFn
|___text
|___ effectFn
如果希望state.text
的改動(dòng)effectFn
不再執(zhí)行,我們就要想辦法改變這個(gè)結(jié)構(gòu)。
state
|___ok
|___ effectFn
此時(shí)無(wú)論你怎樣修改state.text
,effectFn
都不會(huì)執(zhí)行,因?yàn)樗麄儌z之間并沒(méi)有形成依賴關(guān)系。
在副作用函數(shù)執(zhí)行前先將其從與該副作用函數(shù)有關(guān)的依賴集合中刪除怎么樣?
比如前面的例子,形成了:
state
|___ok
|___ effectFn
|___text
|___ effectFn
當(dāng)我們修改state.ok = false
時(shí),effectFn
將會(huì)被執(zhí)行,在執(zhí)行前,我們將effectFn
從與之相關(guān)的依賴集合中刪除,最終形成了一個(gè)光桿司令。
state
但是不要忘記,effectFn
的重新執(zhí)行,又會(huì)觸發(fā)一次依賴收集,結(jié)束后,數(shù)據(jù)結(jié)構(gòu)會(huì)變成:
state
|___ok
|___ effectFn
為了支持這樣的特性,我們需要簡(jiǎn)單的改一下effect
和trigger
函數(shù).
const?effect?=?function?(fn)?{ ??const?effectFn?=?()?=>?{ ????cleanup(effectFn) ????activeEffect?=?effectFn ????fn() ??} ??//?用來(lái)存儲(chǔ)哪些依賴集合包含這個(gè)副作用函數(shù) ??effectFn.deps?=?[] ??effectFn() } function?cleanup?(effectFn)?{ ??for?(let?i?=?0;?i?<?effectFn.deps.length;?i++)?{ ????const?deps?=?effectFn.deps[i] ????deps.delete(effectFn) ??} ??effectFn.deps.length?=?0 }
trigger
//?trigger執(zhí)行依賴 function?trigger(target,?key)?{ ??//?讀取depsMap?其結(jié)構(gòu)是?key?=>?effects ??const?depsMap?=?bucket.get(target); ??if?(!depsMap)?{ ????return; ??} ??//?真正讀取依賴當(dāng)前屬性值key的effects ??const?effects?=?depsMap.get(key); ??//?解決cleanup?執(zhí)行會(huì)無(wú)限執(zhí)行的問(wèn)題 ??const?effectsToRun?=?new?Set(effects) ??//?挨個(gè)執(zhí)行即可 ??effectsToRun.forEach((fn)?=>?fn()); }
最后測(cè)試一把
const?state?=?reactive({ ??ok:?true, ??text:?'hello?world', }); effect(()?=>?{ ??console.log('渲染執(zhí)行') ??document.querySelector('#app').innerHTML?=?state.ok???state.text?:?'not' }) setTimeout(()?=>?{ ??state.ok?=?false?//?頁(yè)面渲染為not ??setTimeout(()?=>?{ ????state.text?=?'other'?//?頁(yè)面依然是not,但是副作用函數(shù)不會(huì)再執(zhí)行。 ??},?1000) },?1000)
支持effect嵌套
為什么要支持effect嵌套
先說(shuō)結(jié)論:因?yàn)榻M件是可以嵌套的,而Vue組件又恰巧是在effect中執(zhí)行的。
來(lái)看看Vue中的組件是怎么執(zhí)行的。
const?Foo?=?{ ??render?()?{ ????return?//?.... ??} } effect(()?=>?{ ??Foo.render() })
而當(dāng)組件發(fā)生嵌套時(shí),就會(huì)存在effect嵌套:
const?Bar?=?{ ??render?()?{ ????return?//?.... ??} } const?Foo?=?{ ??render?()?{ ????return?<Bar?/>?//?... ??} }
最后會(huì)變成這樣:
effect(()?=>?{ ??Foo.render() ??effect(()?=>?{ ????Bar.render() ??}) })
目前的effect存在什么問(wèn)題
先來(lái)試試看目前它的問(wèn)題是什么?。?!
const?state?=?reactive({ ??foo:?true, ??bar:?true }) effect(function?effectFn1?()?{ ??console.log('effectFn1') ??effect(function?effectFn2?()?{ ????console.log('effectFn2') ????console.log('Bar',?state.bar) ??}) ??console.log('Foo',?state.foo) })
根據(jù)上一篇文章的結(jié)論,我們認(rèn)為響應(yīng)式數(shù)據(jù)state
與副作用函數(shù)應(yīng)該會(huì)形成這種數(shù)據(jù)結(jié)構(gòu):
state
|___foo
|___ effectFn1
|___bar
|___ effectFn2
所以首次執(zhí)行時(shí)會(huì)打印出這兩行信息:
當(dāng)我們分別修改foo和bar屬性時(shí)會(huì)發(fā)生什么?
修改bar
effectFn2會(huì)重新執(zhí)行。
const?state?=?reactive({ ??foo:?true, ??bar:?true }) effect(function?effectFn1?()?{ ??console.log('effectFn1') ??effect(function?effectFn2?()?{ ????console.log('effectFn2') ????console.log('Bar',?state.bar) ??}) ??console.log('Foo',?state.foo) }) setTimeout(()?=>?{ ??state.bar?=?false },?1000)
修改foo
effectFn1會(huì)重新執(zhí)行,而effectFn2因?yàn)楸黄淝短姿詴?huì)被間接執(zhí)行。 然而現(xiàn)實(shí)終歸會(huì)告訴我們生活沒(méi)那么美好.
const?state?=?reactive({ ??foo:?true, ??bar:?true }) effect(function?effectFn1?()?{ ??console.log('effectFn1') ??effect(function?effectFn2?()?{ ????console.log('effectFn2') ????console.log('Bar',?state.bar) ??}) ??console.log('Foo',?state.foo) }) setTimeout(()?=>?{ ??state.foo?=?false },?1000)
所以本質(zhì)上形成了這樣的數(shù)據(jù)結(jié)構(gòu),以至于改變foo的值調(diào)用的是effectFn2
。
state
|___foo
|___ effectFn2
|___bar
|___ effectFn2
問(wèn)題出在哪里?
當(dāng)effectFn1
開(kāi)始執(zhí)行的時(shí),activeEffect指向的是effectFn1
。而effectFn1
的執(zhí)行會(huì)間接地導(dǎo)致effectFn2
的執(zhí)行,此時(shí)activeEffect指向的是effectFn2
。
const?effect?=?function?(fn)?{ ??const?effectFn?=?()?=>?{ ????cleanup(effectFn) ????//?問(wèn)題點(diǎn)~~~ ????activeEffect?=?effectFn ????fn() ??} ??//?用來(lái)存儲(chǔ)哪些依賴集合包含這個(gè)副作用函數(shù) ??effectFn.deps?=?[] ??effectFn() }
當(dāng)effectFn2
執(zhí)行完畢時(shí),因?yàn)閍ctiveEffect指向的是effectFn2
。所以foo
自然也就是和effectFn2
建立了聯(lián)系,而不是我們期待的effectFn1
。
effect(function?effectFn1?()?{ ??console.log('effectFn1') ??effect(function?effectFn2?()?{ ????console.log('effectFn2') ????console.log('Bar',?state.bar) ??}) ??console.log('Foo',?state.foo) })
要解決這個(gè)問(wèn)題也很簡(jiǎn)單,我們新維護(hù)一個(gè)注冊(cè)副作用函數(shù)的棧,讓activeEffect指向的是永遠(yuǎn)是棧頂?shù)母弊饔煤瘮?shù)。用上面例子來(lái)模擬一下這個(gè)過(guò)程。
//?第1步:effectFn1執(zhí)行入棧 //?effectFn1?←?activeEffect //?第2步:effectFn2執(zhí)行入棧 /* ??此時(shí)棧變成了 ??effectFn2?←activeEffect ??effectFn1 */ //?第3步:effectFn2執(zhí)行完畢,將effectFn2出棧處理 //?effectFn1?←activeEffect //?第4步:effectFn1執(zhí)行完畢,將effectFn1出棧處理 //?此時(shí)棧已是空的
所以我們很容易對(duì)effect
做出以下改造:
const?bucket?=?new?WeakMap(); const?effectStack?=?[] //?重新定義bucket數(shù)據(jù)類型為WeakMap let?activeEffect; const?effect?=?function?(fn)?{ ??const?effectFn?=?()?=>?{ ????cleanup(effectFn) ????activeEffect?=?effectFn ????//?入棧 ????effectStack.push(effectFn) ????fn() ????//?出棧 ????effectStack.pop() ????activeEffect?=?effectStack[?effectStack.length?-?1?] ??} ??//?用來(lái)存儲(chǔ)哪些依賴集合包含這個(gè)副作用函數(shù) ??effectFn.deps?=?[] ??effectFn() ??console.log(effectStack.length,?'---') ??//?非常重要 ??//?activeEffect?=?null };
再測(cè)試一下上面的例子,一秒鐘后成功的打印了effectFn1和effectFn2
const?state?=?reactive({ ??foo:?true, ??bar:?true }) effect(function?effectFn1?()?{ ??console.log('effectFn1') ??effect(function?effectFn2?()?{ ????console.log('effectFn2') ????console.log('Bar',?state.bar) ??}) ??console.log('Foo',?state.foo) }) setTimeout(()?=>?{ ??state.foo?=?false },?1000)
到此這篇關(guān)于淺析Proxy如何實(shí)現(xiàn)Vue響應(yīng)式的文章就介紹到這了,更多相關(guān)Vue Proxy響應(yīng)式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue實(shí)現(xiàn)短信驗(yàn)證碼登錄功能(流程詳解)
無(wú)論是移動(dòng)端還是pc端登錄或者注冊(cè)界面都會(huì)見(jiàn)到手機(jī)驗(yàn)證碼登錄這個(gè)功能,輸入手機(jī)號(hào),得到驗(yàn)證碼,這篇文章主要介紹了基于vue實(shí)現(xiàn)短信驗(yàn)證碼登錄功能,需要的朋友可以參考下2019-12-12Vue 數(shù)組和對(duì)象更新,但是頁(yè)面沒(méi)有刷新的解決方式
今天小編就為大家分享一篇Vue 數(shù)組和對(duì)象更新,但是頁(yè)面沒(méi)有刷新的解決方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-11-11vue對(duì)storejs獲取的數(shù)據(jù)進(jìn)行處理時(shí)遇到的幾種問(wèn)題小結(jié)
這篇文章主要介紹了vue對(duì)storejs獲取的數(shù)據(jù)進(jìn)行處理時(shí)遇到的幾種問(wèn)題小結(jié),需要的朋友可以參考下2018-03-03springboot?vue接口測(cè)試前端動(dòng)態(tài)增刪表單功能實(shí)現(xiàn)
這篇文章主要為大家介紹了springboot?vue接口測(cè)試前端動(dòng)態(tài)增刪表單功能實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05Vue中使一個(gè)div鋪滿全屏的實(shí)現(xiàn)
最近在項(xiàng)目開(kāi)發(fā)中,就遇到了這個(gè)問(wèn)題,Vue中如何使一個(gè)div鋪滿全屏,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-07-07Vue 項(xiàng)目部署到服務(wù)器的問(wèn)題解決方法
本篇文章主要介紹了Vue 項(xiàng)目部署到服務(wù)器的問(wèn)題解決方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12element UI 中的 el-tree 實(shí)現(xiàn) checkbox&n
在日常項(xiàng)目開(kāi)發(fā)中,會(huì)經(jīng)常遇到,樹(shù)形結(jié)構(gòu)的查詢方式,為了快速方便開(kāi)發(fā),常常會(huì)使用到快捷的ui組件去快速搭樹(shù)形結(jié)構(gòu),這里我用的是 element ui 中的 el-tree,對(duì)element UI 中的 el-tree 實(shí)現(xiàn) checkbox 單選框及 bus 傳遞參數(shù)的方法感興趣的朋友跟隨小編一起看看吧2022-09-09解決elementUI中el-tree樹(shù)形結(jié)構(gòu)中節(jié)點(diǎn)過(guò)濾的問(wèn)題
這篇文章主要介紹了解決elementUI中el-tree樹(shù)形結(jié)構(gòu)中節(jié)點(diǎn)過(guò)濾的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04