關于Vue3中的響應式原理
一、簡介
本章內(nèi)容主要通過具體的簡單示例來分析Vue3是如何實現(xiàn)響應式的。理解本章需要了解Vue3的響應式對象。只注重原理設計層面,細節(jié)不做太多講解。
二、響應核心
1.核心源碼
export class ReactiveEffect<T = any> {
//是否激活
active = true
//依賴列表
deps: Dep[] = []
// can be attached after creation
computed?: boolean
//是否允許遞歸
allowRecurse?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope | null
) {
//將自身添加到一個全局的EffectScope容器中
recordEffectScope(this, scope)
}
run() {
if (!this.active) {
//沒有激活,直接調(diào)用回調(diào)方法
return this.fn()
}
//棧中不存在當前對象
if (!effectStack.includes(this)) {
try {
//推入棧頂,并且置為全局激活對象
effectStack.push((activeEffect = this))
//開啟依賴收集開關
enableTracking()
//位操作符:用于優(yōu)化
trackOpBit = 1 << ++effectTrackDepth
//源碼中maxMarkerBits取30 猜測是因為整數(shù)位運算時是按照32位計算 當1<<31時為負值了,后續(xù)負值的位運算得不到預期結果 所以取的最大30
if (effectTrackDepth <= maxMarkerBits) {
//將當前依賴列表的所有依賴置為"已經(jīng)收集"
initDepMarkers(this)
} else {
//不采用優(yōu)化模式,使用老流程,直接移除依賴的全部狀態(tài)
cleanupEffect(this)
}
//調(diào)用回調(diào)
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
//斷掉依賴關聯(lián)
finalizeDepMarkers(this)
}
//重置位操作狀態(tài)
trackOpBit = 1 << --effectTrackDepth
//重置依賴收集狀態(tài)
resetTracking()
//棧頂出棧
effectStack.pop()
const n = effectStack.length
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
}
}
stop() {
if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
上述ReactiveEffect對象,其實需要關注的就是一個run方法,這個方法設計得十分巧妙,所有動態(tài)響應的本質(zhì)其實都是通過調(diào)用run方法實現(xiàn)的。
比如如下代碼:
let dummy
const counter = reactive({ num: 0 })
let innerfunc = () => dummy = counter.num;
effect(innerfunc)
//下面的賦值,最終會執(zhí)行innerfunc,所以dummy會變成7
counter.num = 7
可能會有疑惑,上述代碼并沒有出現(xiàn)ReactiveEffect類型的對象,它其實是在effect方法中創(chuàng)建的,我們接下來分析下effect方法。
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
//創(chuàng)建對象并傳參
const _effect = new ReactiveEffect(fn)
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
}
if (!options || !options.lazy) {
//執(zhí)行
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
這個方法的簡單用法很簡單,就是創(chuàng)建一個ReactiveEffect類型對象,然后執(zhí)行run方法。
可能對于recordEffectScope方法有疑惑,其實這個方法和響應式無關,它的主要作用是將一個ReactiveEffect對象放入一個effectScope容器對象內(nèi),這個容器對象可以方便快捷的對容器內(nèi)所有的ReactiveEffect對象和其子effectScope調(diào)用stop方法。只關注響應式的話可以不作考慮。
2.逐步分析上述示例代碼
let dummy
//步驟1:創(chuàng)建一個響應式對象
const counter = reactive({ num: 0 })
let innerfunc = () => dummy = counter.num;
//步驟2:調(diào)用effect方法
effect(innerfunc)
//步驟3:修改響應式對象數(shù)據(jù)
counter.num = 7
上述的測試代碼看似就3個步驟,其實內(nèi)部做的東西非常多,我們來跟蹤下運行流程。
步驟1:這一步很簡單,就是單純的創(chuàng)建一個Proxy對象,此時counter對象變成響應式的。
步驟2:effect方法里面最終調(diào)用的是run方法,而run方法主要是將自身掛載到全局激活并入棧,此時調(diào)用回調(diào)方法?;卣{(diào)方法此時為上面innerfunc方法,調(diào)用這個方法會讀取counter.num屬性,讀取響應式對象的屬性會調(diào)用代理攔截處理的get方法,在get方法里面,會收集依賴。此時將依賴存于棧頂?shù)哪莻€ReactiveEffect對象的deps屬性中。
步驟3:當響應式對象的屬性修改后,會觸發(fā)依賴更新,由于觸發(fā)更新的依賴列表里面存在effect方法里面創(chuàng)建的ReactiveEffect對象,所以會重新調(diào)用其run方法,在這兒也就會調(diào)用innerfunc方法。所以dummy屬性就會跟隨counter.num屬性的變化而變化
備注:上述三步驟中,提及了收集依賴和觸發(fā)依賴更新。接下來我們便看一下是如何收集依賴和觸發(fā)依賴更新的。
3.收集依賴和觸發(fā)依賴更新
(1).收集依賴
export function track(target: object, type: TrackOpTypes, key: unknown) {
//是否允許收集
if (!isTracking()) {
return
}
//對象map
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
//依賴map
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
//收集依賴
trackEffects(dep, eventInfo)
}
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
//本一輪調(diào)用新收集的依賴
dep.n |= trackOpBit // set newly tracked
//是否應該被收集
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
//收集依賴
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack(
Object.assign(
{
effect: activeEffect!
},
debuggerEventExtraInfo
)
)
}
}
}
上述代碼是收集依賴的核心代碼??催^我響應式對象文章的話,應該會注意到,在涉及“讀”相關操作時,就會調(diào)用track方法來收集依賴。此時就是調(diào)用的上述track方法。track方法很簡單,主要是找到對應的依賴列表,如果沒有就創(chuàng)建一個依賴列表。
trackEffects里面先只需要關注收集依賴的邏輯,可以很明顯的看到,里面就是一個依賴的雙向添加。至于上面的那些邏輯,最主要的目的是防止重復添加依賴,我會在后面的優(yōu)化環(huán)節(jié)詳細講。
依賴模型存儲模型大致如下:

(2).觸發(fā)依賴更新
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
//獲取依賴map
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
//只需要關注這兒
effect.run()
}
}
}
}
trigger方法在響應式對象的"寫"操作中調(diào)用,這個方法整體上只是根據(jù)不同的依賴更新類型,將依賴添加進一個依賴數(shù)組里面,最終通過triggerEffects方法更新這個依賴數(shù)組里面的依賴。
在triggerEffects方法里面,暫時只需要關注effect.run即可,此時調(diào)用的是ReactiveEffect關聯(lián)的那個回調(diào)方法,這時候也就正確的響應式變化了。
至于effect.scheduler,我會在后續(xù)的計算屬性篇章中講到,這個方法是給計算屬性用的。
三、V3.2的響應式優(yōu)化
上述篇幅只講述了一個整體的響應式變化原理,接下來介紹一下V3.2帶來的響應式性能優(yōu)化。我們先看一下Dep類型的定義
export type Dep = Set<ReactiveEffect> & TrackedMarkers
/**
* wasTracked and newTracked maintain the status for several levels of effect
* tracking recursion. One bit per level is used to define whether the dependency
* was/is tracked.
*/
type TrackedMarkers = {
/**
* wasTracked
*/
w: number
/**
* newTracked
*/
n: number
}
可以看到,依賴列表不是一個簡簡單單的Set集合,它還存在2個用于輔助的屬性。我們創(chuàng)建依賴也是通過createDep方法,實現(xiàn)如下:
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects) as Dep
dep.w = 0
dep.n = 0
return dep
}
我在這兒先說明一下這2個屬性的作用。w屬性用于判斷依賴是否已經(jīng)被收集,n屬性用于判斷依賴在本次調(diào)用中是否用到。可能現(xiàn)在還對此有疑惑,我用以下一個簡單示例來解釋。
let status = true;
let dummy
const depA = reactive({ num: 0 })
const depB = reactive({ num: 10 })
let innerfunc = () => {
dummy = depA.num
if(status){
dummy += depB.num
status = false
}
console.log(dummy);
}
effect(innerfunc)
depA.num = 7
depB.num = 20
//輸出為 10 7
我們來分析以下上述代碼的流程,首先調(diào)用effect方法,會執(zhí)行一次關聯(lián)的innerfunc,此時讀取了depA和depB的num屬性,所以此時ReactiveEffect對象里面的deps屬性存在2個依賴,并且輸出10。當修改depA.num屬性時,會觸發(fā)run方法,此時關注以下代碼:
if (effectTrackDepth <= maxMarkerBits) {
//將當前依賴列表的所有依賴置為"已經(jīng)收集"
initDepMarkers(this)
} else {
//不采用優(yōu)化模式,使用老流程,直接移除依賴的全部狀態(tài)
cleanupEffect(this)
}
因為調(diào)用effect方法時,收集過一次依賴,所以initDepMarkers方法將所有的依賴都標記為已經(jīng)收集。在run方法最后,會調(diào)用innerfunc方法。在innerfunc方法中,這一次調(diào)用中又會去收集依賴,此時關注trackEffects中的以下代碼:
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
//本一輪調(diào)用新收集的依賴
dep.n |= trackOpBit // set newly tracked
//是否應該被收集
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
在run方法中,有一個同樣的判斷effectTrackDepth <= maxMarkerBits,這個判斷是用于控制是否優(yōu)化的,后面會講為什么會存在這個判斷以及為什么maxMarkerBits的取值為30。
在這個收集邏輯中,會將本次回調(diào)中第一次使用到的依賴置為"新增依賴",我們在看innerfunc,此時只會使用到depA,不會使用到depB,因此之前存在的關于depB對象的依賴在本次調(diào)用中沒有用到。
shouldTrack屬性表示依賴是否應該被收集,如果沒有收集,則被收集。此時innerfunc里面輸出的dummy為7。
接下來關注run里面的以下代碼:
if (effectTrackDepth <= maxMarkerBits) {
//斷掉依賴關聯(lián)
finalizeDepMarkers(this)
}
//重置位操作狀態(tài)
trackOpBit = 1 << --effectTrackDepth
//重置依賴收集狀態(tài)
resetTracking()
//棧頂出棧
effectStack.pop()
const n = effectStack.length
activeEffect = n > 0 ? effectStack[n - 1] : undefined
上述代碼存在于run方法里面的finally關鍵字內(nèi),當innerfunc執(zhí)行完后,里面就會執(zhí)行這里。首先便會根據(jù)判斷通過finalizeDepMarkers方法去斷掉依賴關聯(lián)。
我們看以下方法的實現(xiàn):
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit // set was tracked
}
}
}
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect)
} else {
deps[ptr++] = dep
}
// clear bits
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
deps.length = ptr
}
}
這2個方法巧妙的通過位運算將調(diào)用分層。一開始將存在的依賴打上收集標簽,如果在本層中沒有使用到,則斷掉依賴關聯(lián)。當設置depB.num = 20時,首先會找到依賴列表,由于依賴列表中已經(jīng)不存在ReactiveEffect對象了,所以不會觸發(fā)依賴更新,此時不會有新的輸出。
這兒是一個優(yōu)化,斷掉不必要的關聯(lián)依賴,減少方法的調(diào)用。但我們在寫類似代碼時必須非常小心,由于斷掉了依賴關聯(lián),有可能會因為寫法不規(guī)范導致響應失效的情況。
接下來解釋為什么要使用位運算,以及保留不走位運算的邏輯。
關注以下代碼:
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
當每次觸發(fā)依賴更新時,如果都調(diào)用以上方法,會涉及大量的集合刪減操作。
我沒看過Set集合的實現(xiàn),但無非就是數(shù)組或者鏈表。如果使用數(shù)組,增刪操作會導致數(shù)組擴容或者移位,頻繁操作會耗費大量性能,如果是鏈表,也要經(jīng)過一次查找,大量的調(diào)用是會消耗性能的。
那么為什么又要保留這個方法呢,這是因為js引擎在進行整數(shù)位運算時幾乎都是按32位運算的,1 << 31后為負值,得不到預期結果,因此保留原邏輯。但其實這個邏輯幾乎不可能調(diào)到,如果真調(diào)用到這個原始邏輯,我只能說得檢查一下代碼是否規(guī)范了。
四、后話
關于響應式就介紹到這兒,個人理解,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
vue2.0使用Sortable.js實現(xiàn)的拖拽功能示例
本篇文章主要介紹了vue2.0使用Sortable.js實現(xiàn)的拖拽功能示例,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-02-02
vue2更改data里的變量不生效時,深層更改data里的變量問題
這篇文章主要介紹了vue2更改data里的變量不生效時,深層更改data里的變量問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03
JavaScript實現(xiàn)簡單的圖片切換功能(實例代碼)
這篇文章主要介紹了JavaScript實現(xiàn)簡單的圖片切換功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2020-04-04
Element-Ui組件 NavMenu 導航菜單的具體使用
這篇文章主要介紹了Element-Ui組件 NavMenu 導航菜單的具體使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-10-10
Vue項目中使用addRoutes出現(xiàn)問題的解決方法
大家應該都知道可以通過vue-router官方提供的一個api-->addRoutes可以實現(xiàn)路由添加的功能,事實上就也就實現(xiàn)了用戶權限,這篇文章主要給大家介紹了關于Vue項目中使用addRoutes出現(xiàn)問題的解決方法,需要的朋友可以參考下2021-08-08

