六千字詳解!講透 Vue3 響應式是如何實現(xiàn)的
(給前端大學加星標,提升前端技能.)前言
本文使用 ref 對 vue 的響應性進行解讀,僅僅是響應性原理解析,不涉及 vue 組件等概念。
vue 的響應性的實現(xiàn),在 @vue/reactivity 包下,對應的源碼目錄為 packages/reactivity。如何調(diào)試 vue 源碼,可查看該文章[1]
為什么使用 ref 進行講解,而不是 reactive?
ref 比 reactive 的實現(xiàn)簡單,且不需要用到 es6 的 Proxy,僅僅需要使用到對象的 getter 和 setter 函數(shù)
因此,講述響應性原理,我們用簡單的 ref ,盡量減少大家的理解成本
什么是響應性?這部分的響應性定義,來自 vue3 官方文檔[2]
這個術語在程序設計中經(jīng)常被提及,但這是什么意思呢?響應性是一種允許我們以聲明式的方式去適應變化的編程范例。人們通常展示的典型例子,是一份 excel 電子表格 (一個非常好的例子)。
如果將數(shù)字 2 放在第一個單元格中,將數(shù)字 3 放在第二個單元格中并要求提供 SUM,則電子表格會將其計算出來給你。不要驚奇,同時,如果你更新第一個數(shù)字,SUM 也會自動更新。
JavaScript 通常不是這樣工作的——如果我們想用 JavaScript 編寫類似的內(nèi)容:
let?val1?=?2
let?val2?=?3
let?sum?=?val1?+?val2
console.log(sum)?//?5
val1?=?3
console.log(sum)?//?仍然是?5
復制代碼
如果我們更新第一個值,sum 不會被修改。
那么我們?nèi)绾斡?JavaScript 實現(xiàn)這一點呢?
我們這里直接看 @vue/reactive 的測試用例,來看看怎么使用,才會做到響應性的效果
ref 的測試用例
it 包裹的是測試用例的具體內(nèi)容,我們只需要關注回調(diào)里面的代碼即可。
it('should?be?reactive',?()?=>?{
????const?a?=?ref(1)
????let?dummy
????let?calls?=?0
????effect(()?=>?{
????????calls++
????????dummy?=?a.value
????})
????expect(calls).toBe(1)
????expect(dummy).toBe(1)
????a.value?=?2
????expect(calls).toBe(2)
????expect(dummy).toBe(2)
????//?same?value?should?not?trigger
????a.value?=?2
????expect(calls).toBe(2)
????expect(dummy).toBe(2)
})
復制代碼
我們從測試用例中,可以看出有以下幾點結(jié)論:
被 effect 包裹的函數(shù),會自動執(zhí)行一次。
被 effect 函數(shù)包裹的函數(shù)體,擁有了響應性 —— 當 effect 內(nèi)的函數(shù)中的 ref 對象 a.value 被修改時,該函數(shù)會自動重新執(zhí)行。
當 a.value 被設置成同一個值時,函數(shù)并不會自動的重新執(zhí)行。
effect 是什么?
官方文檔中的描述[3]:Vue 通過一個副作用 (effect) 來跟蹤函數(shù)。副作用是一個函數(shù)的包裹器,在函數(shù)被調(diào)用之前就啟動跟蹤。Vue 知道哪個副作用在何時運行,并能在需要時再次執(zhí)行它。
簡單地說,要使一個函數(shù)擁有響應性,就應該將它包裹在(傳入)effect 函數(shù)里。
那么這里也可以稍微猜一下,如果有這么一個 updateDom 函數(shù):
const?a_ref?=?ref('aaaa')
function?updateDom(){
????return?document.body.innerText?=?a_ref.value
}
effect(updateDom)
setTimeout(()=>{
????a_ref.value?=?'bbb'
},1000)
復制代碼
只要用 effect 包裹一下,當 a_ref.value 改變,就會自動設置 document.body.innerText,從而更新界面。
(當然這里也只是猜一下,實際上基本的原理,也與這個差不多,但會復雜很多。由于本文篇幅優(yōu)先,并沒有涉及到這部分)
依賴收集和觸發(fā)更新
要實現(xiàn)響應性,就需要在合適的時機,再次執(zhí)行副作用 effect。如何確定這個合適的時機?就需要依賴收集(英文術語:track)和觸發(fā)更新(英文術語:trigger)
仍然看這個測試用例的例子
it('should?be?reactive',?()?=>?{
????const?a?=?ref(1)
????let?dummy
????let?calls?=?0
????effect(()?=>?{
????????calls++
????????dummy?=?a.value
????})
????expect(calls).toBe(1)
????expect(dummy).toBe(1)
????a.value?=?2
????expect(calls).toBe(2)
????expect(dummy).toBe(2)
????//?same?value?should?not?trigger
????a.value?=?2
????expect(calls).toBe(2)
????expect(dummy).toBe(2)
})
復制代碼
我們已經(jīng)知道,effect 包裹的函數(shù),要在合適的時機被再次執(zhí)行,那么在這個例子中,合適的時機就是,a.value 這個 ref 對象被修改。
由于副作用函數(shù),使用了 a.value,因此副作用函數(shù),依賴 a 這個 ref 變量。我們應該把這個依賴記錄下來。
假如是自己實現(xiàn),可以這么寫:
const?a?=?{
????//?當?a?被訪問時,可以將副作用函數(shù)存儲在?a?對象的?dependency?屬性中,實際上?@vue/reactivity?會稍微復雜一點?
?get?value(){
????????const?fn?=?//?假設有辦法拿到?effect?的副作用函數(shù)
????????//?fn?就是以下這個函數(shù)
????????//?()?=>?{
????????//????calls++
????????//????dummy?=?a.value
????????//?})
????????a.dependence?=?fn
????}
????//?當?a.value?被修改時,可以這么觸發(fā)更新
????set?value(){
????????this.dependence()
????}
}
復制代碼
這樣就可以做到,當 ref 被獲取時,收集依賴(即將副作用函數(shù)保存起來);當 ref 被修改時,觸發(fā)更新(即調(diào)用副作用函數(shù))
當然這個實現(xiàn)非常簡單,實際上還要考慮很多情況,例如:
- 一個副作用函數(shù),可能依賴多個 ref。如 computed,就可能依賴多個 ref,才能算出最終的值,因此依賴是一組的副作用函數(shù)。
- 不是任何時候都收集依賴。僅僅在 effect 包裹的時候,才收集依賴
- 一開始依賴 a 這個 ref 的,但后來不依賴了
- ……
這些情況都是我們沒有考慮進去的,那么,接下來,我們就看看真正的 ref 的實現(xiàn)
概念約定在講解源碼前,我們這里先對一些概念進行約定:
- 副作用對象:在接下來的源碼解析中,特指 effect 函數(shù)內(nèi)部創(chuàng)建的一個對象,類型為 ReactiveEffect(先記住有這么名字即可)。被收集依賴的實際對象。先介紹這么多,后面還會有詳細介紹
- 副作用函數(shù):在接下來的源碼解析中,特指傳入 effect 的函數(shù),也是被觸發(fā)再次執(zhí)行的函數(shù)。
effect(()?=>?{
????calls++
????dummy?=?a.value
})
復制代碼
- 響應式變量:ref、reactive、computed 等函數(shù)返回的變量。
- track:收集依賴
- trigger:觸發(fā)更新
- 副作用對象依賴響應式變量。如:ReactiveEffect 依賴某個 ref
- 響應式變量,擁有多個依賴,依賴的值副作用對象。如:某個 ref 擁有(收集到) n 個 ReactiveEffect 依賴
image-20211231112331231ref 源碼解析通過 ref 的實現(xiàn),看依賴是什么,是怎么被收集的
ref 對象的實現(xiàn)
export?function?ref(value?:?unknown)?{
??return?createRef(value)
}
//?shallowRef,只是將?createRef?的第二個參數(shù)?shallow,標記為?true
export?function?shallowRef(value?:?unknown)?{
??return?createRef(value,?true)
}
function?createRef(rawValue:?unknown,?shallow?=?false)?{
??//?如果已經(jīng)是ref,則直接返回
??if?(isRef(rawValue))?{
????return?rawValue
??}
??return?new?RefImpl(rawValue,?shallow)
}
復制代碼
ref 和 shallowRef, 本質(zhì)都是 RefImpl 對象實例,只是 shallow 屬性不同
為了便于理解,我們可以只關注 ref 的實現(xiàn),即默認 shallow === false
接下來,我們看看 RefImpl 是什么
class?RefImpl?{
??private?_value:?T
??private?_rawValue:?T
??//?用于存儲依賴的副作用函數(shù)
??public?dep?:?Dep?=?undefined
??public?readonly?__v_isRef?=?true
??constructor(value:?T,?public?readonly?_shallow?=?false)?{
????//?保存原始?value?到?_rawValue
????this._rawValue?=?_shallow???value?:?toRaw(value)
????//?convert函數(shù)的作用是,如果?value?是對象,則使用?reactive(value)?處理,否則返回value
????//?因此,將一個對象傳入?ref,實際上也是調(diào)用了?reactive
????this._value?=?_shallow???value?:?convert(value)
??}
??get?value()?{
????//?收集依賴
????trackRefValue(this)
????return?this._value
??}
??set?value(newVal)?{
????newVal?=?this._shallow???newVal?:?toRaw(newVal)
????//?如果值改變,才會觸發(fā)依賴
????if?(hasChanged(newVal,?this._rawValue))?{
??????this._rawValue?=?newVal
??????this._value?=?this._shallow???newVal?:?convert(newVal)
??????//?觸發(fā)依賴
??????triggerRefValue(this,?newVal)
????}
??}
}
復制代碼
在 RefImpl 對象中
- getter 獲取 value 屬性時,trace 收集依賴
- setter 設置 value 屬性時,trigger 觸發(fā)依賴
因此,只有訪問/修改 ref 的 value 屬性,才會收集/觸發(fā)依賴
依賴是怎么被收集的
export?function?trackRefValue(ref:?RefBase<any>)?{
??//?判斷是否需要收集依賴
??if?(isTracking())?{
????ref?=?toRaw(ref)
????//?如果沒有?dep?屬性,則初始化?dep,dep?是一個?Set,存儲副作用函數(shù)
????if?(!ref.dep)?{
??????ref.dep?=?createDep()
????}
????//?收集?effect?依賴
????trackEffects(ref.dep)
??}
}
//?判斷是否需要收集依賴
export?function?isTracking()?{
??//?shouldTrack?是一個全局變量,代表當前是否需要?track?收集依賴
??//?activeEffect?也是個全局變量,代表當前的副作用對象?ReactiveEffect
??return?shouldTrack?&&?activeEffect?!==?undefined
}
復制代碼
為什么需要使用 isTracking,來判斷是否收集依賴?
不是任何情況 ref 被訪問時,都需要收集依賴。例如:
- 沒有被 effect 包裹時,由于沒有副作用函數(shù)(即沒有依賴,activeEffect === undefined),不應該收集依賴
- 某些特殊情況,即使包裹在 effect,也不應該收集依賴(即 shouldTrack === false)。如:組件生命周期執(zhí)行、組件 setup 執(zhí)行
ref.dep 有什么作用?
ref.dep 的類型是Set ,關于 ReactiveEffect 的細節(jié)會在后面詳細闡述
ref.dep 用于存儲副作用對象,這些副作用對象,依賴該 ref,ref 被修改時就會觸發(fā)
我們再來看看 trackEffects:
//?代表當前的副作用?effect
let?activeEffect:?ReactiveEffect?|?undefined
export?function?trackEffects(
??dep:?Dep
)?{
??//?這個是局部變量的?shouldTrack,跟上一部分的全局?shouldTrack?不一樣
??let?shouldTrack?=?false
??//?已經(jīng)?track?收集過依賴,就可以跳過了
??shouldTrack?=?!dep.has(activeEffect!)
??if?(shouldTrack)?{
????//?收集依賴,將?effect?存儲到?dep
????dep.add(activeEffect!)
????//?同時?effect?也記錄一下?dep
????//?用于?trigger?觸發(fā)?effect?后,刪除?dep?里面對應的?effect,即?dep.delete(activeEffect)
????activeEffect!.deps.push(dep)
??}
}
復制代碼
收集依賴,就是把 activeEffect(當前的副作用對象),保存到 ref.dep 中(當觸發(fā)依賴時,遍歷 ref.dep 執(zhí)行 effect )
然后把 ref.dep,也保存到 effect.deps 中(用于在觸發(fā)依賴后, ref.dep.delete(effect),雙向刪除依賴)
image-20211230205303018依賴是怎么被觸發(fā)的
看完 track 收集依賴,那看看依賴是怎么被觸發(fā)的
export?function?triggerRefValue(ref:?RefBase<any>,?newVal?:?any)?{
??//?ref?可能是?reactive?對象的某個屬性的值
??//?這時候在?triggerRefValue(this,?newVal)?時取?this,拿到的是一個?reactive?對象
??//?需要獲取?Proxy?代理背后的真實值?ref?對象
??ref?=?toRaw(ref)
??//?有依賴才觸發(fā)?effect
??if?(ref.dep)?{
?????triggerEffects(ref.dep)
??}
}
復制代碼
再來看看 triggerEffects
export?function?triggerEffects(
??dep:?Dep?|?ReactiveEffect[]
)?{
??//?循環(huán)遍歷?dep,去取每個依賴的副作用對象?ReactiveEffect
??for?(const?effect?of?isArray(dep)???dep?:?[...dep])?{
????//?默認不允許遞歸,即當前?effect?副作用函數(shù),如果遞歸觸發(fā)當前?effect,會被忽略
????if?(effect?!==?activeEffect?||?effect.allowRecurse)?{
??????//?effect.scheduler可以先不管,ref?和?reactive?都沒有
??????if?(effect.scheduler)?{
????????effect.scheduler()
??????}?else?{
????????//?執(zhí)行?effect?的副作用函數(shù)
????????effect.run()
??????}
????}
??}
}
復制代碼
這里省略了一些代碼,這樣結(jié)構(gòu)更清晰。
當 ref 被修改時,會 trigger 觸發(fā)依賴,即執(zhí)行了 ref.dep 里的所有副作用函數(shù)(effect.run 運行副作用函數(shù))
為什么默認不允許遞歸?
const?foo?=?ref([])
effect(()=>{
????foo.value.push(1)
})
復制代碼
在這個副作用函數(shù)中,即會使用到 foo.value(getter 收集依賴),又會修改 foo 數(shù)組(觸發(fā)依賴)。如果允許遞歸,會無限循環(huán)。
至此,ref 依賴收集和觸發(fā)的邏輯,已經(jīng)比較清晰了。
那么,接下來,我們需要進一步了解的是,effect 函數(shù)、ReactiveEffect 副作用對象、副作用函數(shù),它們是什么,它們之間有什么關系?
effect 函數(shù)
我們來看一下 effect 的實現(xiàn)
//?傳入一個?fn?函數(shù)
export?function?effect<T?=?any>(
??fn:?()?=>?T
){
??//?參數(shù)?fn,可能也是一個?effect,所以要獲取到最初始的?fn?參數(shù)
??if?((fn?as?ReactiveEffectRunner).effect)?{
????fn?=?(fn?as?ReactiveEffectRunner).effect.fn
??}
??//?創(chuàng)建?ReactiveEffect?對象
??const?_effect?=?new?ReactiveEffect(fn)
??_effect.run()
??
??const?runner?=?_effect.run.bind(_effect)
??runner.effect?=?_effect
??return?runner
}
復制代碼
effect 函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù),我們稱之為副作用函數(shù)
effect 函數(shù)內(nèi)部,會創(chuàng)建 ReactiveEffect 對象,我們稱之為副作用對象
effect 函數(shù),返回一個 runner,是一個函數(shù),直接調(diào)用就是調(diào)用副作用函數(shù);runner 的屬性 effect,保存著它對應的 ReactiveEffect 對象 。
因此,它們的關系如下:
effect 函數(shù)的入?yún)楦弊饔煤瘮?shù),在 effect 函數(shù)內(nèi)部會創(chuàng)建副作用對象
我們繼續(xù)深入看看 ReactiveEffect 對象的實現(xiàn)
ReactiveEffect 副作用對象
該部分(effect.run 函數(shù))代碼有比較大的刪減,點擊查看未刪減的源碼[4]
為什么要刪減這部分代碼?
在 vue 3.2 版本以后,effect.run 做了優(yōu)化,提升性能,其中涉及到位運算。
優(yōu)化方案在極端的情況下(effect 非常多次嵌套),會降級到原來的老方案(優(yōu)化前,3.2版本前的方案)
因此,為了便于理解,我這里先介紹優(yōu)化前的方案,深入了解,并闡述該方案的缺點, 以便更好地理解為什么需要進行優(yōu)化。
刪減部分為優(yōu)化后的方案,這部分的方案會在下一小節(jié)進行介紹。
下面是 ReactiveEffect 代碼解析:
//?全局公用的?effect?棧,由于可以?effect?嵌套,因此需要用棧保存?ReactiveEffect?副作用對象
const?effectStack:?ReactiveEffect[]?=?[]
export?class?ReactiveEffectany>?{
??active?=?true
????
??//?存儲?Dep?對象,如上一小節(jié)的?ref.dep
??deps:?Dep[]?=?[]
??constructor(
????public?fn:?()?=>?T,
????public?scheduler:?EffectScheduler?|?null?=?null,
????scope?:?EffectScope?|?null
??)?{
????//?可以暫時不看,與?effectScope?API?相關?https://v3.cn.vuejs.org/api/effect-scope.html#effectscope
????//?將當前?ReactiveEffect?副作用對象,記錄到?effectScope?中
????//?當?effectScope.stop()?被調(diào)用時,所有的?ReactiveEffect?對象都會被?stop
????recordEffectScope(this,?scope)
??}
??run()?{
????//?如果當前?ReactiveEffect?副作用對象,已經(jīng)在棧里了,就不需要再處理了
????if?(!effectStack.includes(this))?{
??????try?{
????????//?保存上一個的?activeEffect,因為?effect?可以嵌套
????????effectStack.push((activeEffect?=?this))
????????//?開啟?shouldTrack?開關,緩存上一個值
????????enableTracking()
????????//?在該?effect?所在的所有?dep?中,清除?effect,下面會詳細闡述
????????cleanupEffect(this)
??????????
????????//?執(zhí)行副作用函數(shù),執(zhí)行過程中,又會?track?當前的?effect?進來,依賴重新被收集
????????return?this.fn()
??????}?finally?{
????????//?關閉shouldTrack開關,恢復上一個值
????????resetTracking()
????????//?恢復上一個的?activeEffect
????????effectStack.pop()
????????const?n?=?effectStack.length
????????activeEffect?=?n?>?0???effectStack[n?-?1]?:?undefined
??????}
????}
??}
}
//?允許?track
export?function?enableTracking()?{
??//?trackStack?是個全局的棧,由于?effect?可以嵌套,所以是否?track?的標記,也需要用棧保存
??trackStack.push(shouldTrack)
??//?打開全局?shouldTrack?開關
??shouldTrack?=?true
}
//?重置上一個?track?狀態(tài)
export?function?resetTracking()?{
??const?last?=?trackStack.pop()
??//?恢復上一個?track?狀態(tài)
??shouldTrack?=?last?===?undefined???true?:?last
}
復制代碼
為什么要用棧保存 effect 和 track 狀態(tài)?
因為effect可能會嵌套,需要保存之前的狀態(tài),effect執(zhí)行完成后恢復
cleanupEffect 做了什么?
回顧下圖:
image-20220102234627353effect.deps,也存儲著響應式變量的 dep(dep 是一個依賴集合, ReactiveEffect 對象的集合),目的是在effect 執(zhí)行后,在所有的 dep 中刪除當前執(zhí)行過的 effect,雙向刪除
刪除代碼如下:
function?cleanupEffect(effect:?ReactiveEffect)?{
??const?{?deps?}?=?effect
??if?(deps.length)?{
????for?(let?i?=?0;?i???????//?從?ref.dep?中刪除?ReactiveEffect
??????deps[i].delete(effect)
????}
????//?從?ReactiveEffect.deps?中刪除?dep
????deps.length?=?0
??}
}
復制代碼
刪除的 ReactiveEffect 如何被重新收集?
在 cleanupEffect 中,在各個 dep 中,刪除該 ReactiveEffect 對象。
在執(zhí)行 this.fn() 時,執(zhí)行副作用函數(shù),副作用函數(shù)的執(zhí)行中,當使用到響應式變量(如 ref.value)時,又會 trackEffect,重新收集依賴。
為什么要先刪除,再重新收集依賴?
因為執(zhí)行前后的依賴可能不一致,考慮一下情況:
const?switch?=?ref(true)
const?foo?=?ref('foo')
effect(?()?=?{
??if(switch.value){
????console.log(foo.value)
??}else{
????console.log('else?condition')
??}
})
switch.value?=?false
復制代碼
當 switch 為 true 時,triggerEffect,雙向刪除后,執(zhí)行副作用函數(shù),switch、foo 會重新收集到依賴 effect
當 switch 變成 false 后,triggerEffect,雙向刪除后,執(zhí)行副作用函數(shù),僅有 switch 能重新收集到依賴 effect
image-20211231110604009由于 effect 副作用函數(shù)執(zhí)行前后,依賴的響應式變量(這里是 ref )可能不一致,因此 vue 會先刪除全部依賴,再重新收集。
細心的你,可能會發(fā)現(xiàn):自己寫 vue 代碼時,很少會出現(xiàn)前后依賴不一致的情況。那既然這樣,刪除全部依賴這個實現(xiàn)就有優(yōu)化的空間,能不能只刪除失效的依賴呢?
依賴更新算法優(yōu)化
該優(yōu)化是 vue 3.2 版本引入的,原因即上一小節(jié)所說的,可以只刪除失效的依賴。并且在極端的嵌套深度下,能夠降級到 cleanupEffect 方法,對所有依賴進行刪除。
先想想,假如是自己實現(xiàn),要怎么寫好呢?
- 不使用 cleanupEffect 刪除所有依賴
- 執(zhí)行副作用函數(shù)前,給 ReactiveEffect 依賴的響應式變量,加上 was 的標記(was 是 vue 給的名稱,過去的意思)
- 執(zhí)行
this.fn(),track 重新收集依賴時,給 ReactiveEffect 的每個依賴,加上 new 的標記 - 最后,對失效(有 was 但是沒有 new)依賴進行刪除
為什么是標記在響應式對象,而不是 ReactiveEffect ?
再回顧一下響應式變量和 ReactiveEffect 的關系:
image-20211231112331231ReactiveEffect 依賴響應式變量(ref),響應式變量(ref)擁有多個 ReactiveEffect 依賴。
只刪除失效的依賴。就要確定哪些依賴(響應式變量)需要被刪除(實際上是響應式變量的 dep 被刪除)
因此,需要在響應式變量上做標記,對已經(jīng)不依賴的響應式變量,將它們的 dep,從 ReactiveEffect.deps 中刪除
如何給響應式變量做標記
實現(xiàn)如下:
export?const?initDepMarkers?=?({?deps?}:?ReactiveEffect)?=>?{
??if?(deps.length)?{
????//?循環(huán)?deps,對每個?dep?進行標記
????for?(let?i?=?0;?i???????//?標記?dep?為?was,w?是?was?的意思
??????deps[i].w?|=?trackOpBit
????}
??}
}
復制代碼
這部分代碼其實比較難理解,尤其是使用了位運算符,如果一開始就解析這些代碼的話,很容易就勸退了。
下面我們對問題進行分析:
為什么這里標記的是 dep?
這里的 dep,對于 ref,就是 ref.dep,它是一個 Set 。
dep 跟 ref 的關系是一一對應的,一個 ref僅僅有一個 dep,因此,標記在 dep 和 標記在 ref,是等價的
那為什么不在響應式變量上標記呢?
因為響應式變量的類型有幾種:ref、computed、reactive,它們都使用 dep 對象存儲依賴,對它們都有的 dep 對象進行標記,可以將標記代碼更好的進行復用(否則要判斷不同的類型,執(zhí)行不同的標記邏輯)。
如果未來新增一種響應式變量,只需要也是用 dep 進行存儲依賴即可
這個按位與位運算的作用是什么?
先來看看 dep 的真實結(jié)構(gòu),它其實還有兩個屬性 w 和 n:
export?type?Dep?=?Set?&?TrackedMarkers
type?TrackedMarkers?=?{
??/**
???*?wasTracked,代表副作用函數(shù)執(zhí)行前被?track?過
???*/
??w:?number
??/**
???*?newTracked,代表副作用函數(shù)執(zhí)行后被?track
???*/
??n:?number
}
復制代碼
那這個 w 和 n 是怎么做標記的?我們先來看看位運算做了什么,不了解位運算的同學 ,可以先看看這里的介紹[5]
dep.w?|=?trackOpBit?//?即?dep.w?=?dep.w?|?trackOpBit
復制代碼
image-20220103205852303將響應式變量標記,就是將對應整數(shù)的二進制位,設置成 1
dep.n 的標記方法也是如此。
為什么要使用位運算?
- 位運算速度快
- 只需要使用一個 number 類型的數(shù)據(jù),就能存儲不同深度的標記(was / new)
如果不使用位運算,需要實現(xiàn)同樣的標記能力,需要用數(shù)組存儲不同深度的標記,數(shù)據(jù)結(jié)構(gòu)如下:
export?type?Dep?=?Set?&?TrackedMarkers
type?TrackedMarkers?=?{
??/**
???*?wasTrackedList,代表副作用函數(shù)執(zhí)行前被?track?過
???*?設計為數(shù)組,是因為?effect?可以嵌套,代表響應式變量在所在的?effect?深度(嵌套層級)中是否被?track
???*/
??wasTrackedList:?boolean[]
??/**
???*?newTracked,代表副作用函數(shù)執(zhí)行后被?track
???*?設計為數(shù)組,是因為?effect?可以嵌套,代表響應式變量在所在的?effect?深度(嵌套層級)中是否被?track
???*/
??newTrackedList:?boolean[]
}
復制代碼
使用數(shù)組存儲標記位,修改處理沒有直接位運算快。由于 vue 每次執(zhí)行副作用函數(shù)(一個頁面有非常多的副作用函數(shù)),都需要頻繁進行標記,這開銷也是非常大的。因此,這里使用了運算符,提升了標記的速度,也節(jié)省了運行內(nèi)存
trackOpBit 是什么?
trackOpBit 是代表當前操作的位,它是由 effect 嵌套深度決定的。
//?全局變量嵌套深度一開始為?0?
effectTrackDepth?=?0
//?每次執(zhí)行?effect?副作用函數(shù)前,全局變量嵌套深度會自增?1,執(zhí)行完成?effect?副作用函數(shù)后會自減
trackOpBit?=?1?<++effectTrackDepth
復制代碼
當深度為 1 時,trackOpBit 是 2(二進制:00000010),操作的是第二位,將 dep.w 的第二位變成 1
因此如圖所說,dep.w 的第一位是不使用的
為什么最大標記嵌套深度為 30?
從圖中我們可以看到,深度受存儲類型的位數(shù)限制,否則就會溢出。
在JavaScript內(nèi)部,數(shù)值都是以64位浮點數(shù)的形式儲存,但是做位運算的時候,是以32位帶符號的整數(shù)進行運算的,并且返回值也是一個32位帶符號的整數(shù)。
1?<30
//?1073741824
1?<31
//?-2147483648,溢出
復制代碼
因此,深度最大為 30,超過 30,則需要降級方案,使用全部清除再全部重新收集依賴的方案
判斷響應式變量是否被標記
export?const?wasTracked?=?(dep:?Dep):?boolean?=>?(dep.w?&?trackOpBit)?>?0
export?const?newTracked?=?(dep:?Dep):?boolean?=>?(dep.n?&?trackOpBit)?>?0
復制代碼
使用 wasTracked 和 newTracked 判斷 dep 是否在當前深度被標記
trackOpBit 是一個全局變量,根據(jù)當前深度生成的
image-20220103210036377如圖,如果需要判斷深度為 2 時(trackOpBit 第 3 位為 1),是否被標記,僅當 dep.w 的第 3 位為 1 時, wasTracked 或 newTracked 才會返回 true
vue 通過這樣巧妙的位運算,快速算出依賴在當前深度是否被標記
副作用對象的優(yōu)化實現(xiàn)
//?當前?effect?的嵌套深度,每次執(zhí)行會?++effectTrackDepth
let?effectTrackDepth?=?0
//?最大的?effect?嵌套層數(shù)為?30
const?maxMarkerBits?=?30??????
//?位運算操作的第?trackOpBit?位
export?let?trackOpBit?=?1
export?class?ReactiveEffectany>?{
??run()?{
????if?(!effectStack.includes(this))?{
??????try?{
????????//?省略代碼:?保存上一個?activeEffect
????????
????????//?trackOpBit:?根據(jù)深度生成?trackOpBit
????????trackOpBit?=?1?<++effectTrackDepth
????????//?maxMarkerBits:?可支持的最大嵌套深度,為?30
????????//?這里就是之前說到的,正常情況下使用優(yōu)化方案,極端嵌套場景下,使用降級方案
????????if?(effectTrackDepth?<=?maxMarkerBits)?{
??????????//?標記所有的?dep?為?was
??????????initDepMarkers(this)
????????}?else?{
??????????//?降級方案,刪除所有的依賴,再重新收集
??????????cleanupEffect(this)
????????}
?????????//?執(zhí)行過程中標記新的?dep?為?new
????????return?this.fn()
??????}?finally?{
????????if?(effectTrackDepth?<=?maxMarkerBits)?{
??????????//?對失效依賴進行刪除
??????????finalizeDepMarkers(this)
????????}
??//?恢復上一次的狀態(tài)
????????//?嵌套深度?effectTrackDepth?自減
????????//?重置操作的位數(shù)
????????trackOpBit?=?1?<--effectTrackDepth
????????//?省略代碼:?恢復上一個?activeEffect
??????}
????}
??}
}
復制代碼
整體的思路如下:
如果當前深度不超過 30,使用優(yōu)化方案
- 執(zhí)行副作用函數(shù)前,給 ReactiveEffect 依賴的響應式變量,加上 was 的標記(was 是 vue 給的名稱,表示過去依賴)
- 執(zhí)行
this.fn(),track 重新收集依賴時,給 ReactiveEffect 的每個依賴,加上 new 的標記 - 對失效依賴進行刪除(有 was 但是沒有 new)
- 恢復上一個深度的狀態(tài)
如果深度超過 30 ,超過部分,使用降級方案:
- 雙向刪除 ReactiveEffect 副作用對象的所有依賴(effect.deps.length = 0)
- 執(zhí)行
this.fn(),track 重新收集依賴時 - 恢復上一個深度的狀態(tài)
標記 ReactiveEffect 的所有的 dep 為 was 的實現(xiàn):
export?const?initDepMarkers?=?({?deps?}:?ReactiveEffect)?=>?{
??if?(deps.length)?{
????for?(let?i?=?0;?i???????deps[i].w?|=?trackOpBit?//?遍歷每個?dep?標記為?was
????}
??}
}
復制代碼
對失效依賴進行刪除的實現(xiàn)如下(有 was 但是沒有 new):

export?const?finalizeDepMarkers?=?(effect:?ReactiveEffect)?=>?{
??const?{?deps?}?=?effect
??if?(deps.length)?{
????let?ptr?=?0
????for?(let?i?=?0;?i???????const?dep?=?deps[i]
??????//有?was?標記但是沒有?new?標記,應當刪除
??????if?(wasTracked(dep)?&&?!newTracked(dep))?{
????????dep.delete(effect)
??????}?else?{
????????//?需要保留的依賴,放到數(shù)據(jù)的較前位置,因為在最后會刪除較后位置的所有依賴
????????deps[ptr++]?=?dep
??????}
??????//?清理?was?和?new?標記,將它們對應深度的?bit,置為?0
??????dep.w?&=?~trackOpBit
??????dep.n?&=?~trackOpBit
????}
????//?刪除依賴,只保留需要的
????deps.length?=?ptr
??}
}
復制代碼
參考文章- vue 官方文檔[6]
- vue-next 源碼[7]
