淺析Proxy如何實現(xiàn)Vue響應(yīng)式
前言
在面試官:Vue3響應(yīng)式系統(tǒng)都不會寫,還敢說精通?中我們實現(xiàn)了一個最基本的響應(yīng)式系統(tǒng)。
它包含以下功能:
- 借助Proxy將一個對象obj變成響應(yīng)式數(shù)據(jù),攔截其get和set操作。
- 通過effect注冊副作用函數(shù),并在首次執(zhí)行副作用函數(shù)時完成obj對象的依賴收集(track)。
- 當(dāng)數(shù)據(jù)發(fā)生變化的時候,第2步注冊的副作用函數(shù)會重新執(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無值意味著沒有執(zhí)行effect函數(shù),無法收集依賴,直接return掉 ??if?(!activeEffect)?{ ????return ??} ??//?每個target在bucket中都是一個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ì)是個Set結(jié)構(gòu),即一個key可以存在多個effect函數(shù),被多個effect所依賴 ????depsMap.set(key,?(deps?=?new?Set())) ??} ??//?將激活的effectFn存進桶中 ??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) ??//?挨個執(zhí)行即可 ??effects?&&?effects.forEach((fn)?=>?fn()) } //?統(tǒng)一對外暴露響應(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) ????} ??}) }
測試一下
const?state?=?reactive({ ??name:?'fatfish', ??age:?100 }) //?effect1 effect(()?=>?{ ??console.log(state.name,?'name') }) //?effect2 effect(()?=>?{ ??console.log(state.age,?'age') }) state.name?=?'fatfish2'?//?因為name屬性發(fā)生變化了,effect1將會重新執(zhí)行,打印出的name是fatfish2
看起來還不錯,不過他還存在很多缺陷和不足,比如:
- 分支切換會導(dǎo)致不必要的effect執(zhí)行損耗
- effect不支持嵌套注冊副作用函數(shù)
- ...
咱們挨個看看,這都是些啥...
支持分支切換
什么是分支切換?
按照上文的結(jié)論,這段代碼執(zhí)行后會形成這樣的數(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
后,頁面將渲染為"not"。意味著后續(xù)無論text
如何變化,頁面都永遠(yuǎn)只可能是"not"。
所以當(dāng)我們修改text
的值時,副作用函數(shù)重新執(zhí)行是沒有必要的。
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?//?此時頁面變成了not ??setTimeout(()?=>?{ ????state.text?=?'other'?//?頁面依然是not,但是副作用函數(shù)卻還會執(zhí)行一次 ??},?1000) },?1000)
如何解決?
修改state.text
,副作用函數(shù)會執(zhí)行是因為state
與其形成的數(shù)據(jù)結(jié)構(gòu)是這樣的。
state
|___ok
|___ effectFn
|___text
|___ effectFn
如果希望state.text
的改動effectFn
不再執(zhí)行,我們就要想辦法改變這個結(jié)構(gòu)。
state
|___ok
|___ effectFn
此時無論你怎樣修改state.text
,effectFn
都不會執(zhí)行,因為他們倆之間并沒有形成依賴關(guān)系。
在副作用函數(shù)執(zhí)行前先將其從與該副作用函數(shù)有關(guān)的依賴集合中刪除怎么樣?
比如前面的例子,形成了:
state
|___ok
|___ effectFn
|___text
|___ effectFn
當(dāng)我們修改state.ok = false
時,effectFn
將會被執(zhí)行,在執(zhí)行前,我們將effectFn
從與之相關(guān)的依賴集合中刪除,最終形成了一個光桿司令。
state
但是不要忘記,effectFn
的重新執(zhí)行,又會觸發(fā)一次依賴收集,結(jié)束后,數(shù)據(jù)結(jié)構(gòu)會變成:
state
|___ok
|___ effectFn
為了支持這樣的特性,我們需要簡單的改一下effect
和trigger
函數(shù).
const?effect?=?function?(fn)?{ ??const?effectFn?=?()?=>?{ ????cleanup(effectFn) ????activeEffect?=?effectFn ????fn() ??} ??//?用來存儲哪些依賴集合包含這個副作用函數(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í)行會無限執(zhí)行的問題 ??const?effectsToRun?=?new?Set(effects) ??//?挨個執(zhí)行即可 ??effectsToRun.forEach((fn)?=>?fn()); }
最后測試一把
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?//?頁面渲染為not ??setTimeout(()?=>?{ ????state.text?=?'other'?//?頁面依然是not,但是副作用函數(shù)不會再執(zhí)行。 ??},?1000) },?1000)
支持effect嵌套
為什么要支持effect嵌套
先說結(jié)論:因為組件是可以嵌套的,而Vue組件又恰巧是在effect中執(zhí)行的。
來看看Vue中的組件是怎么執(zhí)行的。
const?Foo?=?{ ??render?()?{ ????return?//?.... ??} } effect(()?=>?{ ??Foo.render() })
而當(dāng)組件發(fā)生嵌套時,就會存在effect嵌套:
const?Bar?=?{ ??render?()?{ ????return?//?.... ??} } const?Foo?=?{ ??render?()?{ ????return?<Bar?/>?//?... ??} }
最后會變成這樣:
effect(()?=>?{ ??Foo.render() ??effect(()?=>?{ ????Bar.render() ??}) })
目前的effect存在什么問題
先來試試看目前它的問題是什么!!!
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)該會形成這種數(shù)據(jù)結(jié)構(gòu):
state
|___foo
|___ effectFn1
|___bar
|___ effectFn2
所以首次執(zhí)行時會打印出這兩行信息:
當(dāng)我們分別修改foo和bar屬性時會發(fā)生什么?
修改bar
effectFn2會重新執(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會重新執(zhí)行,而effectFn2因為被其嵌套所以會被間接執(zhí)行。 然而現(xià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) }) setTimeout(()?=>?{ ??state.foo?=?false },?1000)
所以本質(zhì)上形成了這樣的數(shù)據(jù)結(jié)構(gòu),以至于改變foo的值調(diào)用的是effectFn2
。
state
|___foo
|___ effectFn2
|___bar
|___ effectFn2
問題出在哪里?
當(dāng)effectFn1
開始執(zhí)行的時,activeEffect指向的是effectFn1
。而effectFn1
的執(zhí)行會間接地導(dǎo)致effectFn2
的執(zhí)行,此時activeEffect指向的是effectFn2
。
const?effect?=?function?(fn)?{ ??const?effectFn?=?()?=>?{ ????cleanup(effectFn) ????//?問題點~~~ ????activeEffect?=?effectFn ????fn() ??} ??//?用來存儲哪些依賴集合包含這個副作用函數(shù) ??effectFn.deps?=?[] ??effectFn() }
當(dāng)effectFn2
執(zhí)行完畢時,因為activeEffect指向的是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) })
要解決這個問題也很簡單,我們新維護一個注冊副作用函數(shù)的棧,讓activeEffect指向的是永遠(yuǎn)是棧頂?shù)母弊饔煤瘮?shù)。用上面例子來模擬一下這個過程。
//?第1步:effectFn1執(zhí)行入棧 //?effectFn1?←?activeEffect //?第2步:effectFn2執(zhí)行入棧 /* ??此時棧變成了 ??effectFn2?←activeEffect ??effectFn1 */ //?第3步:effectFn2執(zhí)行完畢,將effectFn2出棧處理 //?effectFn1?←activeEffect //?第4步:effectFn1執(zhí)行完畢,將effectFn1出棧處理 //?此時棧已是空的
所以我們很容易對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?] ??} ??//?用來存儲哪些依賴集合包含這個副作用函數(shù) ??effectFn.deps?=?[] ??effectFn() ??console.log(effectStack.length,?'---') ??//?非常重要 ??//?activeEffect?=?null };
再測試一下上面的例子,一秒鐘后成功的打印了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如何實現(xiàn)Vue響應(yīng)式的文章就介紹到這了,更多相關(guān)Vue Proxy響應(yīng)式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue 數(shù)組和對象更新,但是頁面沒有刷新的解決方式
今天小編就為大家分享一篇Vue 數(shù)組和對象更新,但是頁面沒有刷新的解決方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11vue對storejs獲取的數(shù)據(jù)進行處理時遇到的幾種問題小結(jié)
這篇文章主要介紹了vue對storejs獲取的數(shù)據(jù)進行處理時遇到的幾種問題小結(jié),需要的朋友可以參考下2018-03-03springboot?vue接口測試前端動態(tài)增刪表單功能實現(xiàn)
這篇文章主要為大家介紹了springboot?vue接口測試前端動態(tài)增刪表單功能實現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-05-05element UI 中的 el-tree 實現(xiàn) checkbox&n
在日常項目開發(fā)中,會經(jīng)常遇到,樹形結(jié)構(gòu)的查詢方式,為了快速方便開發(fā),常常會使用到快捷的ui組件去快速搭樹形結(jié)構(gòu),這里我用的是 element ui 中的 el-tree,對element UI 中的 el-tree 實現(xiàn) checkbox 單選框及 bus 傳遞參數(shù)的方法感興趣的朋友跟隨小編一起看看吧2022-09-09解決elementUI中el-tree樹形結(jié)構(gòu)中節(jié)點過濾的問題
這篇文章主要介紹了解決elementUI中el-tree樹形結(jié)構(gòu)中節(jié)點過濾的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04