Vue3響應(yīng)式對(duì)象是如何實(shí)現(xiàn)的(2)
前言
在Vue3響應(yīng)式對(duì)象是如何實(shí)現(xiàn)的(1)中,我們已經(jīng)從功能上實(shí)現(xiàn)了一個(gè)響應(yīng)式對(duì)象。如果僅僅滿足于功能實(shí)現(xiàn),我們就可以止步于此了。但在上篇中,我們僅考慮了最簡(jiǎn)單的情況,想要完成一個(gè)完整可用的響應(yīng)式,需要我們繼續(xù)對(duì)細(xì)節(jié)深入思考。在特定場(chǎng)景下,是否存在BUG?是否還能繼續(xù)優(yōu)化?
分支切換的優(yōu)化
在上篇中,收集副作用函數(shù)是利用get
自動(dòng)收集。那么被get
自動(dòng)收集的副作用函數(shù),是否有可能會(huì)產(chǎn)生多余的觸發(fā)呢?或者說(shuō),我們其實(shí)進(jìn)行了多余的收集呢?同樣,還是從一個(gè)例子入手。
let activeEffect function effect(fn) { activeEffect = fn fn() } const objsMap = new WeakMap() const data = { text: 'hello vue', ok: true } // (1) const obj = new Proxy(data, { get(target, key) { track(target, key) return target[key] }, set(target, key, newValue) { target[key] = newValue trigger(target, key) return true } }) function track(target, key) { if(!activeEffect) return let propsMap = objsMap.get(target) if(!propsMap) { objsMap.set(target, (propsMap = new Map())) } let fns = propsMap.get(key) if(!fns) { propsMap.set(key, (fns = new Set())) } fns.add(activeEffect) } function trigger(target, key) { const propsMap = objsMap.get(target) if(!propsMap) return const fns = propsMap.get(key) fns && fns.forEach(fn => fn()) } function fn() { document.body.innerText = obj.ok ? obj.text : 'ops...' // (2) console.log('Done!') } effect(fn)
這段代碼中,我們做了(1)(2)兩處更改。我們?cè)冢?)處給響應(yīng)式對(duì)象新增加了一個(gè)boolean
類型的屬性ok
,在(2)處我們利用ok
的真值,來(lái)選擇將誰(shuí)賦值給document.body.innerText
?,F(xiàn)在,我們將obj.ok
的值置為false
,這就意味著,document.body.innerText
的值不再依賴于obj.text
,而直接取字符串'ops...'
。
此時(shí),我們要能夠注意到一件事,雖然document.body.innerText
的值不再依賴于obj.text
了,但由于ok
的初值是true
,也就意味著在ok
的值沒(méi)有改變時(shí),document.body.innerText
的值依賴于obj.text
,更進(jìn)一步說(shuō),這個(gè)函數(shù)已經(jīng)被obj.text
當(dāng)作自己的副作用函數(shù)收集了。這會(huì)導(dǎo)致什么呢?
我們更改了obj.text
的值,這會(huì)觸發(fā)副作用函數(shù)。但此時(shí)由于ok
的值為false
,界面上顯示的內(nèi)容沒(méi)有發(fā)生任何改變。也就是說(shuō),此時(shí)修改obj.text
觸發(fā)的副作用函數(shù)的更新是不必要的。
這部分有些繞,讓我們通過(guò)畫(huà)圖來(lái)嘗試說(shuō)明。當(dāng)ok
為true
時(shí),數(shù)據(jù)結(jié)構(gòu)的狀態(tài)如圖所示:
從圖中可以看到,obj.text
和obj.ok
都收集了同一個(gè)副作用函數(shù)fn
。這也解釋了為什么即使我們將obj.ok
的值為false
,更改obj.text
仍然會(huì)觸發(fā)副作用函數(shù)fn
。
我們希望的理想狀況是,當(dāng)ok
為false
時(shí),副作用函數(shù)fn
被從obj.text
的副作用函數(shù)收集器中刪除,數(shù)據(jù)結(jié)構(gòu)的狀態(tài)能改變?yōu)槿缦聽(tīng)顟B(tài)。
這就要求我們能夠在每次執(zhí)行副作用函數(shù)前,將該副作用函數(shù)從相關(guān)的副作用函數(shù)收集器中刪除,再重新建立聯(lián)系。為了實(shí)現(xiàn)這一點(diǎn),就要求我們記錄哪些副作用函數(shù)收集器收集了該副作用函數(shù)。
let activeEffect function cleanup(effectFn) { // (3) for(let i = 0; i < effectFn.deps.length; i++) { const fns = effectFn.deps[i] fns.delete(effectFn) } effectFn.deps.length = 0 } function effect(fn) { const effectFn = () => { cleanup(effectFn) activeEffect = effectFn fn() } effectFn.deps = [] // (1) effectFn() } const objsMap = new WeakMap() const data = { text: 'hello vue', ok: true } const obj = new Proxy(data, { get(target, key) { track(target, key) return target[key] }, set(target, key, newValue) { target[key] = newValue trigger(target, key) return true } }) function track(target, key) { if(!activeEffect) return let propsMap = objsMap.get(target) if(!propsMap) { objsMap.set(target, (propsMap = new Map())) } let fns = propsMap.get(key) if(!fns) { propsMap.set(key, (fns = new Set())) } fns.add(activeEffect) activeEffect.deps.push(fns) // (2) } function trigger(target, key) { const propsMap = objsMap.get(target) if(!propsMap) return const fns = propsMap.get(key) fns && fns.forEach(fn => fn()) } function fn() { document.body.innerText = obj.ok ? obj.text : 'ops...' console.log('Done!') } effect(fn)
在這段代碼中,我們?cè)黾恿?處改動(dòng)。為了記錄副作用函數(shù)被哪些副作用函數(shù)收集器收集,我們?cè)冢?)處給每個(gè)副作用函數(shù)掛載了一個(gè)deps
,用于記錄該副作用函數(shù)被誰(shuí)收集。在(2)處,副作用函數(shù)被收集時(shí),我們記錄副作用函數(shù)收集器。在(3)處,我們新增了cleanup
函數(shù),從含有該副作用函數(shù)的副作用函數(shù)收集器中,刪除該副作用函數(shù)。
看上去好像沒(méi)啥問(wèn)題了,但是運(yùn)行代碼會(huì)發(fā)現(xiàn)產(chǎn)生了死循環(huán)。問(wèn)題出在哪呢?
以下面這段代碼為例:
const set = new Set([1]) set.forEach(item => { set.delete(1) set.add(1) console.log('Done!') })
是的,這段代碼會(huì)產(chǎn)生死循環(huán)。原因是ECMAScript對(duì)Set.prototype.forEach
的規(guī)范中明確,使用forEach
遍歷Set
時(shí),如果有值被直接添加到該Set
上,則forEach
會(huì)再次訪問(wèn)該值。
const effectFn = () => { cleanup(effectFn) // (1) activeEffect = effectFn fn() // (2) }
同理,我們的代碼中,當(dāng)effectFn
被執(zhí)行時(shí),(1)處的cleanup
清除副作用函數(shù),就相當(dāng)于set.delete
;而(2)處執(zhí)行副作用函數(shù)fn
時(shí),會(huì)觸發(fā)依賴收集,將副作用函數(shù)又加入到了副作用函數(shù)收集器中,相當(dāng)于set.add
,從而造成死循環(huán)。
解決的方法也很簡(jiǎn)單,我們只需要避免在原Set
上直接進(jìn)行遍歷即可。
const set = new Set([1]) const otherSet = new Set(set) otherSet.forEach(item => { set.delete(1) set.add(1) console.log('Done!') })
在上例中,我們復(fù)制了set
到otherset
中,otherset
僅會(huì)執(zhí)行set.length
次。按照這個(gè)思路,修改我們的代碼。
let activeEffect function cleanup(effectFn) { for(let i = 0; i < effectFn.deps.length; i++) { const fns = effectFn.deps[i] fns.delete(effectFn) } effectFn.deps.length = 0 } function effect(fn) { const effectFn = () => { cleanup(effectFn) activeEffect = effectFn fn() } effectFn.deps = [] effectFn() } const objsMap = new WeakMap() const data = { text: 'hello vue', ok: true } const obj = new Proxy(data, { get(target, key) { track(target, key) return target[key] }, set(target, key, newValue) { target[key] = newValue trigger(target, key) return true } }) function track(target, key) { if(!activeEffect) return let propsMap = objsMap.get(target) if(!propsMap) { objsMap.set(target, (propsMap = new Map())) } let fns = propsMap.get(key) if(!fns) { propsMap.set(key, (fns = new Set())) } fns.add(activeEffect) activeEffect.deps.push(fns) } function trigger(target, key) { const propsMap = objsMap.get(target) if(!propsMap) return const fns = propsMap.get(key) const otherFns = new Set(fns) // (1) otherFns.forEach(fn => fn()) } function fn() { document.body.innerText = obj.ok ? obj.text : 'ops...' console.log('Done!') } effect(fn)
在(1)處我們新增了一個(gè)otherFns
,復(fù)制了fns
用來(lái)遍歷。讓我們?cè)賮?lái)看看結(jié)果。
①處,更改obj.ok
的值為false
,改變了頁(yè)面的顯示,沒(méi)有導(dǎo)致死循環(huán)。②處,當(dāng)obj.ok
為false
時(shí),副作用函數(shù)沒(méi)有執(zhí)行。至此,我們完成了針對(duì)分支切換場(chǎng)景下的優(yōu)化。
副作用函數(shù)嵌套產(chǎn)生的BUG
我們繼續(xù)從功能角度考慮,前面我們的副作用函數(shù)還是不夠復(fù)雜,實(shí)際應(yīng)用中(如組件嵌套渲染),副作用函數(shù)是可以發(fā)生嵌套的。
我們舉個(gè)簡(jiǎn)單的嵌套示例:
let t1, t2 effect(function effectFn1() { console.log('effectFn1') effect(function effectFn2() { console.log('effectFn2') t2 = obj.bar }) t1 = obj.foo })
這段代碼中,我們將effectFn2
嵌入了effectFn1
中,將obj.foo
賦值給t1
,obj.bar
賦值給t2
。從響應(yīng)式的功能上看,如果我們修改obj.foo
的值,應(yīng)該會(huì)觸發(fā)effectFn1
的執(zhí)行,且間接觸發(fā)effectFn2
執(zhí)行。
修改obj.foo
的值僅觸發(fā)了effectFn2
的更新,這與我們的預(yù)期不符。既然是effect
這里出了問(wèn)題,讓我們?cè)賮?lái)過(guò)一遍effect
部分的代碼,看看能不能發(fā)現(xiàn)點(diǎn)什么。
let activeEffect // (1) function cleanup(effectFn) { for(let i = 0; i < effectFn.deps.length; i++) { const fns = effectFn.deps[i] fns.delete(effectFn) } effectFn.deps.length = 0 } function effect(fn) { const effectFn = () => { cleanup(effectFn) activeEffect = effectFn fn() // (2) } effectFn.deps = [] effectFn() }
仔細(xì)思考后,不難發(fā)現(xiàn)問(wèn)題所在。我們?cè)冢?)處定義了一個(gè)全局變量activeEffect
用于副作用函數(shù)注冊(cè),這意味著同一時(shí)刻,我們僅能注冊(cè)一個(gè)副作用函數(shù)。在(2)處執(zhí)行了fn
,此時(shí)注意,在我們給出的副作用函數(shù)嵌套示例中,effectFn1
是先執(zhí)行effectFn2
,再執(zhí)行t1 = obj.foo
。也就是說(shuō),此時(shí)activeEffect
注冊(cè)的副作用函數(shù)已經(jīng)由effectFn1
變?yōu)榱?code>effectFn2。因此,當(dāng)執(zhí)行到t1 = obj.foo
時(shí),track
收集的activeEffect
已經(jīng)是被effectFn2
覆蓋過(guò)的。所以,修改obj.foo
,trigger
觸發(fā)的就是effectFn2
了。
要解決這個(gè)問(wèn)題也很簡(jiǎn)單,既然后出現(xiàn)的要先被收集,后進(jìn)先出,用棧解決就好了。
let activeEffect const effectStack = [] // (1) function cleanup(effectFn) { for(let i = 0; i < effectFn.deps.length; i++) { const fns = effectFn.deps[i] fns.delete(effectFn) } effectFn.deps.length = 0 } function effect(fn) { const effectFn = () => { cleanup(effectFn) activeEffect = effectFn effectStack.push(effectFn) fn() // (2) effectStack.pop() activeEffect = effectStack[effectStack.length - 1] } effectFn.deps = [] effectFn() }
這段代碼中,我們?cè)冢?)處定義了一個(gè)棧effectStack
。不管(2)處如何更改activeEffect
的內(nèi)容,都會(huì)被effectStack[effectStack.length - 1]
回滾到原先正確的副作用函數(shù)上。
運(yùn)行的結(jié)果和我們的預(yù)期一致,到此為止,我們已經(jīng)完成了對(duì)嵌套副作用函數(shù)的處理。
自增/自減操作產(chǎn)生的BUG
這里還存在一個(gè)隱蔽的BUG,還和之前一樣,我們修改effect
。
effect(() => obj.foo++)
很簡(jiǎn)單的副作用函數(shù),這會(huì)有什么問(wèn)題呢?執(zhí)行一下看看。
很不幸,棧溢出了。這個(gè)副作用函數(shù)僅包含一個(gè)obj.foo++
,所以可以確定,棧溢出就是由這個(gè)自增運(yùn)算引起的。接下來(lái)的問(wèn)題就是,這么簡(jiǎn)單的自增操作,怎么會(huì)引起棧溢出呢?為了更好的說(shuō)明問(wèn)題,讓我們先來(lái)拆解問(wèn)題。
effect(() => obj.foo = obj.foo + 1)
這段代碼中obj.foo = obj.foo + 1
就等價(jià)于obj.foo++
。這樣拆開(kāi)之后問(wèn)題一下就清楚了。這里同時(shí)進(jìn)行了obj.foo
的get
和set
操作。先讀取obj.foo
,收集了副作用函數(shù),再設(shè)置obj.foo
,觸發(fā)了副作用函數(shù),而這個(gè)副作用函數(shù)中obj.foo
又要被讀取,如此往復(fù),產(chǎn)生了死循環(huán)。為了驗(yàn)證這一點(diǎn),我們打印執(zhí)行的副作用函數(shù)。
上面的打印結(jié)果印證了我們的想法。造成這個(gè)BUG的主要原因是,當(dāng)get
和set
操作同時(shí)存在時(shí),我們收集和觸發(fā)的都是同一個(gè)副作用函數(shù)。這里我們只需要添加一個(gè)守衛(wèi)條件:當(dāng)觸發(fā)的副作用函數(shù)正在被執(zhí)行時(shí),該副作用函數(shù)則不必再被執(zhí)行。
function trigger(target, key) { const propsMap = objsMap.get(target) if(!propsMap) return const fns = propsMap.get(key) const otherFns = new Set() fns && fns.forEach(fn => { if(fn !== activeEffect) { // (1) otherFns.add(fn) } }) otherFns.forEach(fn => fn()) }
如此一來(lái),相同的副作用函數(shù)僅會(huì)被觸發(fā)一次,避免了產(chǎn)生死循環(huán)。最后,我們驗(yàn)證一下即可。
到此這篇關(guān)于Vue3響應(yīng)式對(duì)象是如何實(shí)現(xiàn)的的文章就介紹到這了,更多相關(guān)Vue3響應(yīng)式對(duì)象內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決vue-router路由攔截造成死循環(huán)問(wèn)題
這篇文章主要介紹了解決vue-router路由攔截造成死循環(huán)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08vue項(xiàng)目tween方法實(shí)現(xiàn)返回頂部的示例代碼
這篇文章主要介紹了vue項(xiàng)目tween方法實(shí)現(xiàn)返回頂部,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03vue keep-alive列表頁(yè)緩存 詳情頁(yè)返回上一頁(yè)不刷新,定位到之前位置
這篇文章主要介紹了vue keep-alive列表頁(yè)緩存 詳情頁(yè)返回上一頁(yè)不刷新,定位到之前位置,本文通過(guò)實(shí)例代碼效果圖展示給大家介紹的非常詳細(xì),需要的朋友可以參考下2019-11-11vue-router中的hash和history兩種模式的區(qū)別
大家都知道vue-router有兩種模式,hash模式和history模式,這里來(lái)談?wù)剉ue-router中的hash和history兩種模式的區(qū)別。感興趣的朋友一起看看吧2018-07-07vue實(shí)現(xiàn)標(biāo)簽頁(yè)切換/制作tab組件詳細(xì)教程
在項(xiàng)目開(kāi)發(fā)中需要使用vue實(shí)現(xiàn)tab頁(yè)簽切換功能,所以這里總結(jié)下,這篇文章主要給大家介紹了關(guān)于vue實(shí)現(xiàn)標(biāo)簽頁(yè)切換/制作tab組件的相關(guān)資料,需要的朋友可以參考下2023-11-11vue不通過(guò)路由直接獲取url中參數(shù)的方法示例
通過(guò)url傳遞參數(shù)是我們?cè)陂_(kāi)發(fā)中經(jīng)常用到的一種傳參方法,但通過(guò)url傳遞后改如果獲取呢?下面這篇文章主要給大家介紹了關(guān)于vue如何不通過(guò)路由直接獲取url中參數(shù)的相關(guān)資料,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-08-08vue+flv.js+SpringBoot+websocket實(shí)現(xiàn)視頻監(jiān)控與回放功能
vue+springboot的項(xiàng)目,需要在頁(yè)面展示出??档挠脖P(pán)錄像機(jī)連接的攝像頭的實(shí)時(shí)監(jiān)控畫(huà)面以及回放功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2022-02-02Vue如何實(shí)現(xiàn)自動(dòng)觸發(fā)功能
這篇文章主要介紹了Vue如何實(shí)現(xiàn)自動(dòng)觸發(fā)功能,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01Element中Select選擇器的實(shí)現(xiàn)
本文主要介紹了Element中Select選擇器的實(shí)現(xiàn),文中根據(jù)實(shí)例編碼詳細(xì)介紹的十分詳盡,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03vue動(dòng)態(tài)合并單元格并添加小計(jì)合計(jì)功能示例
這篇文章主要給大家介紹了關(guān)于vue動(dòng)態(tài)合并單元格并添加小計(jì)合計(jì)功能的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11