六千字講透 Vue3 響應(yīng)式是如何實現(xiàn)的

作者:candyTong
https://juejin.cn/post/7048970987500470279
前言
為什么使用 ref 進行講解,而不是 reactive?
什么是響應(yīng)性?
這部分的響應(yīng)性定義,來自 vue3?官方文檔[2]
let?val1?=?2
let?val2?=?3
let?sum?=?val1?+?val2
console.log(sum)?//?5
val1?=?3
console.log(sum)?//?仍然是?5
復(fù)制代碼
ref 的測試用例
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)
})
復(fù)制代碼
被 effect 包裹的函數(shù),會自動執(zhí)行一次。
被 effect 函數(shù)包裹的函數(shù)體,擁有了響應(yīng)性?—— 當 effect 內(nèi)的函數(shù)中的 ref 對象 a.value 被修改時,該函數(shù)會自動重新執(zhí)行。
當 a.value 被設(shè)置成同一個值時,函數(shù)并不會自動的重新執(zhí)行。
effect 是什么?
const?a_ref?=?ref('aaaa')
function?updateDom(){
????return?document.body.innerText?=?a_ref.value
}
effect(updateDom)
setTimeout(()=>{
????a_ref.value?=?'bbb'
},1000)
復(fù)制代碼
依賴收集和觸發(fā)更新
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)
})
復(fù)制代碼
const?a?=?{
????//?當?a?被訪問時,可以將副作用函數(shù)存儲在?a?對象的?dependency?屬性中,實際上?@vue/reactivity?會稍微復(fù)雜一點?
?get?value(){
????????const?fn?=?//?假設(shè)有辦法拿到?effect?的副作用函數(shù)
????????//?fn?就是以下這個函數(shù)
????????//?()?=>?{
????????//????calls++
????????//????dummy?=?a.value
????????//?})
????????a.dependence?=?fn
????}
????//?當?a.value?被修改時,可以這么觸發(fā)更新
????set?value(){
????????this.dependence()
????}
}
復(fù)制代碼
一個副作用函數(shù),可能依賴多個 ref。如 computed,就可能依賴多個 ref,才能算出最終的值,因此依賴是一組的副作用函數(shù)。 不是任何時候都收集依賴。僅僅在 effect 包裹的時候,才收集依賴 一開始依賴 a 這個 ref 的,但后來不依賴了 微信搜索readdot,關(guān)注后回復(fù)視頻教程獲取23種精品資料 ……
概念約定
副作用對象:在接下來的源碼解析中,特指 effect 函數(shù)內(nèi)部創(chuàng)建的一個對象,類型為 ReactiveEffect(先記住有這么名字即可)。被收集依賴的實際對象。先介紹這么多,后面還會有詳細介紹 副作用函數(shù):在接下來的源碼解析中,特指傳入 effect 的函數(shù),也是被觸發(fā)再次執(zhí)行的函數(shù)。
effect(()?=>?{
????calls++
????dummy?=?a.value
})
復(fù)制代碼
響應(yīng)式變量:ref、reactive、computed 等函數(shù)返回的變量。 track:收集依賴 trigger:觸發(fā)更新 副作用對象依賴響應(yīng)式變量。如:ReactiveEffect 依賴某個 ref 響應(yīng)式變量,擁有多個依賴,依賴的值副作用對象。如:某個 ref 擁有(收集到) n 個 ReactiveEffect 依賴

ref 源碼解析
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)
}
復(fù)制代碼
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)
????}
??}
}
復(fù)制代碼
getter 獲取 value 屬性時,trace 收集依賴 setter 設(shè)置 value 屬性時,trigger 觸發(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
}
復(fù)制代碼
為什么需要使用 isTracking,來判斷是否收集依賴?
沒有被 effect 包裹時,由于沒有副作用函數(shù)(即沒有依賴,activeEffect === undefined),不應(yīng)該收集依賴 某些特殊情況,即使包裹在 effect,也不應(yīng)該收集依賴(即 shouldTrack === false)。如:組件生命周期執(zhí)行、組件 setup 執(zhí)行
ref.dep 有什么作用?
Set?,關(guān)于 ReactiveEffect 的細節(jié)會在后面詳細闡述//?代表當前的副作用?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?里面對應(yīng)的?effect,即?dep.delete(activeEffect)
????activeEffect!.deps.push(dep)
??}
}
復(fù)制代碼

依賴是怎么被觸發(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)
??}
}
復(fù)制代碼
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()
??????}
????}
??}
}
復(fù)制代碼
為什么默認不允許遞歸?
const?foo?=?ref([])
effect(()=>{
????foo.value.push(1)
})
復(fù)制代碼
effect 函數(shù)
//?傳入一個?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
}
復(fù)制代碼
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?相關(guān)?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?開關(guān),緩存上一個值
????????enableTracking()
????????//?在該?effect?所在的所有?dep?中,清除?effect,下面會詳細闡述
????????cleanupEffect(this)
??????????
????????//?執(zhí)行副作用函數(shù),執(zhí)行過程中,又會?track?當前的?effect?進來,依賴重新被收集
????????return?this.fn()
??????}?finally?{
????????//?關(guān)閉shouldTrack開關(guān),恢復(fù)上一個值
????????resetTracking()
????????//?恢復(fù)上一個的?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?開關(guān)
??shouldTrack?=?true
}
//?重置上一個?track?狀態(tài)
export?function?resetTracking()?{
??const?last?=?trackStack.pop()
??//?恢復(fù)上一個?track?狀態(tài)
??shouldTrack?=?last?===?undefined???true?:?last
}
復(fù)制代碼
為什么要用棧保存 effect 和 track 狀態(tài)?
cleanupEffect 做了什么?

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
??}
}
復(fù)制代碼
刪除的 ReactiveEffect 如何被重新收集?
this.fn()?時,執(zhí)行副作用函數(shù),副作用函數(shù)的執(zhí)行中,當使用到響應(yīng)式變量(如 ref.value)時,又會 trackEffect,重新收集依賴。為什么要先刪除,再重新收集依賴?
const?switch?=?ref(true)
const?foo?=?ref('foo')
effect(?()?=?{
??if(switch.value){
????console.log(foo.value)
??}else{
????console.log('else?condition')
??}
})
switch.value?=?false
復(fù)制代碼

依賴更新算法優(yōu)化
不使用 cleanupEffect 刪除所有依賴 執(zhí)行副作用函數(shù)前,給 ReactiveEffect 依賴的響應(yīng)式變量,加上 was 的標記(was 是 vue 給的名稱,過去的意思) 執(zhí)行? this.fn(),track 重新收集依賴時,給 ReactiveEffect 的每個依賴,加上 new 的標記最后,對失效(有 was 但是沒有 new)依賴進行刪除
為什么是標記在響應(yīng)式對象,而不是 ReactiveEffect ?

如何給響應(yīng)式變量做標記
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
????}
??}
}
復(fù)制代碼
為什么這里標記的是 dep?
Set?。那為什么不在響應(yīng)式變量上標記呢?
這個按位與位運算的作用是什么?
export?type?Dep?=?Set ?&?TrackedMarkers
type?TrackedMarkers?=?{
??/**
???*?wasTracked,代表副作用函數(shù)執(zhí)行前被?track?過
???*/
??w:?number
??/**
???*?newTracked,代表副作用函數(shù)執(zhí)行后被?track
???*/
??n:?number
}
復(fù)制代碼
dep.w?|=?trackOpBit?//?即?dep.w?=?dep.w?|?trackOpBit
復(fù)制代碼

為什么要使用位運算?
位運算速度快 只需要使用一個 number 類型的數(shù)據(jù),就能存儲不同深度的標記(was / new)
export?type?Dep?=?Set ?&?TrackedMarkers
type?TrackedMarkers?=?{
??/**
???*?wasTrackedList,代表副作用函數(shù)執(zhí)行前被?track?過
???*?設(shè)計為數(shù)組,是因為?effect?可以嵌套,代表響應(yīng)式變量在所在的?effect?深度(嵌套層級)中是否被?track
???*/
??wasTrackedList:?boolean[]
??/**
???*?newTracked,代表副作用函數(shù)執(zhí)行后被?track
???*?設(shè)計為數(shù)組,是因為?effect?可以嵌套,代表響應(yīng)式變量在所在的?effect?深度(嵌套層級)中是否被?track
???*/
??newTrackedList:?boolean[]
}
復(fù)制代碼
trackOpBit 是什么?
//?全局變量嵌套深度一開始為?0?
effectTrackDepth?=?0
//?每次執(zhí)行?effect?副作用函數(shù)前,全局變量嵌套深度會自增?1,執(zhí)行完成?effect?副作用函數(shù)后會自減
trackOpBit?=?1?<++effectTrackDepth
復(fù)制代碼
為什么最大標記嵌套深度為 30?
1?<30
//?1073741824
1?<31
//?-2147483648,溢出
復(fù)制代碼
判斷響應(yīng)式變量是否被標記
export?const?wasTracked?=?(dep:?Dep):?boolean?=>?(dep.w?&?trackOpBit)?>?0
export?const?newTracked?=?(dep:?Dep):?boolean?=>?(dep.n?&?trackOpBit)?>?0
復(fù)制代碼
wasTracked?和?newTracked?判斷 dep 是否在當前深度被標記
dep.w?的第 3 位為 1 時,?wasTracked?或?newTracked?才會返回 true副作用對象的優(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)
????????}
??//?恢復(fù)上一次的狀態(tài)
????????//?嵌套深度?effectTrackDepth?自減
????????//?重置操作的位數(shù)
????????trackOpBit?=?1?<--effectTrackDepth
????????//?省略代碼:?恢復(fù)上一個?activeEffect
??????}
????}
??}
}
復(fù)制代碼
如果當前深度不超過 30,使用優(yōu)化方案
執(zhí)行副作用函數(shù)前,給 ReactiveEffect 依賴的響應(yīng)式變量,加上 was 的標記(was 是 vue 給的名稱,表示過去依賴) 執(zhí)行? this.fn(),track 重新收集依賴時,給 ReactiveEffect 的每個依賴,加上 new 的標記對失效依賴進行刪除(有 was 但是沒有 new) 恢復(fù)上一個深度的狀態(tài) 如果深度超過 30 ,超過部分,使用降級方案:
雙向刪除?ReactiveEffect 副作用對象的所有依賴(effect.deps.length = 0) 執(zhí)行? this.fn(),track 重新收集依賴時恢復(fù)上一個深度的狀態(tài)
export?const?initDepMarkers?=?({?deps?}:?ReactiveEffect)?=>?{
??if?(deps.length)?{
????for?(let?i?=?0;?i???????deps[i].w?|=?trackOpBit?//?遍歷每個?dep?標記為?was
????}
??}
}
復(fù)制代碼

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

評論
圖片
表情
