自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

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

開發(fā) 前端
本文使用 ref 對 vue 的響應(yīng)性進(jìn)行解讀,僅僅是響應(yīng)性原理解析,不涉及 vue 組件等概念。

?

前言

本文使用 ref 對 vue 的響應(yīng)性進(jìn)行解讀,僅僅是響應(yīng)性原理解析,不涉及 vue 組件等概念。

vue 的響應(yīng)性的實(shí)現(xiàn),在 @vue/reactivity 包下,對應(yīng)的源碼目錄為 packages/reactivity。如何調(diào)試 vue 源碼,可查看該文章[1]

為什么使用 ref 進(jìn)行講解,而不是 reactive?

ref 比 reactive 的實(shí)現(xiàn)簡單,且不需要用到 es6 的 Proxy,僅僅需要使用到對象的 getter 和 setter 函數(shù)

因此,講述響應(yīng)性原理,我們用簡單的 ref ,盡量減少大家的理解成本

什么是響應(yīng)性?

這部分的響應(yīng)性定義,來自 vue3 官方文檔[2]

這個(gè)術(shù)語在程序設(shè)計(jì)中經(jīng)常被提及,但這是什么意思呢?響應(yīng)性是一種允許我們以聲明式的方式去適應(yīng)變化的編程范例。人們通常展示的典型例子,是一份 excel 電子表格 (一個(gè)非常好的例子)。

如果將數(shù)字 2 放在第一個(gè)單元格中,將數(shù)字 3 放在第二個(gè)單元格中并要求提供 SUM,則電子表格會將其計(jì)算出來給你。不要驚奇,同時(shí),如果你更新第一個(gè)數(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
復(fù)制代碼

如果我們更新第一個(gè)值,sum 不會被修改。

那么我們?nèi)绾斡?JavaScript 實(shí)現(xiàn)這一點(diǎn)呢?

我們這里直接看 @vue/reactive 的測試用例,來看看怎么使用,才會做到響應(yīng)性的效果

ref 的測試用例

it 包裹的是測試用例的具體內(nèi)容,我們只需要關(guān)注回調(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)
})
復(fù)制代碼

我們從測試用例中,可以看出有以下幾點(diǎn)結(jié)論:

  1. 被 effect 包裹的函數(shù),會自動執(zhí)行一次。
  2. 被 effect 函數(shù)包裹的函數(shù)體,擁有了響應(yīng)性 —— 當(dāng) effect 內(nèi)的函數(shù)中的 ref 對象 a.value 被修改時(shí),該函數(shù)會自動重新執(zhí)行。
  3. 當(dāng) a.value 被設(shè)置成同一個(gè)值時(shí),函數(shù)并不會自動的重新執(zhí)行。

 effect 是什么?

官方文檔中的描述[3]:Vue 通過一個(gè)副作用 (effect) 來跟蹤函數(shù)。副作用是一個(gè)函數(shù)的包裹器,在函數(shù)被調(diào)用之前就啟動跟蹤。Vue 知道哪個(gè)副作用在何時(shí)運(yùn)行,并能在需要時(shí)再次執(zhí)行它。

簡單地說,要使一個(gè)函數(shù)擁有響應(yīng)性,就應(yīng)該將它包裹在(傳入)effect 函數(shù)里。

那么這里也可以稍微猜一下,如果有這么一個(gè) updateDom 函數(shù):

const a_ref = ref('aaaa')
function updateDom(){
return document.body.innerText = a_ref.value
}
effect(updateDom)
setTimeout(()=>{
a_ref.value = 'bbb'
},1000)
復(fù)制代碼

只要用 effect 包裹一下,當(dāng) a_ref.value 改變,就會自動設(shè)置 document.body.innerText,從而更新界面。

(當(dāng)然這里也只是猜一下,實(shí)際上基本的原理,也與這個(gè)差不多,但會復(fù)雜很多。由于本文篇幅優(yōu)先,并沒有涉及到這部分)

依賴收集和觸發(fā)更新

要實(shí)現(xiàn)響應(yīng)性,就需要在合適的時(shí)機(jī),再次執(zhí)行副作用 effect。如何確定這個(gè)合適的時(shí)機(jī)?就需要依賴收集(英文術(shù)語:track)和觸發(fā)更新(英文術(shù)語:trigger)

仍然看這個(gè)測試用例的例子

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ù)制代碼

我們已經(jīng)知道,effect 包裹的函數(shù),要在合適的時(shí)機(jī)被再次執(zhí)行,那么在這個(gè)例子中,合適的時(shí)機(jī)就是,a.value 這個(gè) ref 對象被修改。

由于副作用函數(shù),使用了 a.value,因此副作用函數(shù),依賴 a 這個(gè) ref 變量。我們應(yīng)該把這個(gè)依賴記錄下來。

假如是自己實(shí)現(xiàn),可以這么寫:

const a = {
// 當(dāng) a 被訪問時(shí),可以將副作用函數(shù)存儲在 a 對象的 dependency 屬性中,實(shí)際上 @vue/reactivity 會稍微復(fù)雜一點(diǎn)
get value(){
const fn = // 假設(shè)有辦法拿到 effect 的副作用函數(shù)
// fn 就是以下這個(gè)函數(shù)
// () => {
// calls++
// dummy = a.value
// })
a.dependence = fn
}
// 當(dāng) a.value 被修改時(shí),可以這么觸發(fā)更新
set value(){
this.dependence()
}
}
復(fù)制代碼

這樣就可以做到,當(dāng) ref 被獲取時(shí),收集依賴(即將副作用函數(shù)保存起來);當(dāng) ref 被修改時(shí),觸發(fā)更新(即調(diào)用副作用函數(shù))

當(dāng)然這個(gè)實(shí)現(xiàn)非常簡單,實(shí)際上還要考慮很多情況,例如:

  • 一個(gè)副作用函數(shù),可能依賴多個(gè) ref。如 computed,就可能依賴多個(gè) ref,才能算出最終的值,因此依賴是一組的副作用函數(shù)。
  • 不是任何時(shí)候都收集依賴。僅僅在 effect 包裹的時(shí)候,才收集依賴
  • 一開始依賴 a 這個(gè) ref 的,但后來不依賴了
  • ……

這些情況都是我們沒有考慮進(jìn)去的,那么,接下來,我們就看看真正的 ref 的實(shí)現(xiàn)

概念約定

在講解源碼前,我們這里先對一些概念進(jìn)行約定:

  • 副作用對象:在接下來的源碼解析中,特指 effect 函數(shù)內(nèi)部創(chuàng)建的一個(gè)對象,類型為 ReactiveEffect(先記住有這么名字即可)。被收集依賴的實(shí)際對象。先介紹這么多,后面還會有詳細(xì)介紹
  • 副作用函數(shù):在接下來的源碼解析中,特指傳入 effect 的函數(shù),也是被觸發(fā)再次執(zhí)行的函數(shù)。
effect(() => {
effect(() => {
effect(() => {
calls++
dummy = a.value
})
復(fù)制代碼
// 當(dāng) a 被訪問時(shí),可以將副作用函數(shù)存儲在 a 對象的 dependency 屬性中,實(shí)際上 @vue/reactivity 會稍微復(fù)雜一點(diǎn)
get value(){
const a = {
  • 響應(yīng)式變量:ref、reactive、computed 等函數(shù)返回的變量。
  • get value(){
  • trigger:觸發(fā)更新
  • 副作用對象依賴響應(yīng)式變量。如:ReactiveEffect 依賴某個(gè) ref
  • 響應(yīng)式變量,擁有多個(gè)依賴,依賴的值副作用對象。如:某個(gè) ref 擁有(收集到) n 個(gè) ReactiveEffect 依賴

image-20211231112331231

ref 源碼解析

通過 ref 的實(shí)現(xiàn),看依賴是什么,是怎么被收集的

ref 對象的實(shí)現(xiàn)

export function ref(value?: unknown) {
return createRef(value)
}
// shallowRef,只是將 createRef 的第二個(gè)參數(shù) shallow,標(biāo)記為 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ù)制代碼

ref 和 shallowRef, 本質(zhì)都是 RefImpl 對象實(shí)例,只是 shallow 屬性不同

為了便于理解,我們可以只關(guān)注 ref 的實(shí)現(xiàn),即默認(rèn) shallow === false

接下來,我們看看 RefImpl 是什么

class RefImpl<T> {
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
// 因此,將一個(gè)對象傳入 ref,實(shí)際上也是調(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ù)制代碼

在 RefImpl 對象中

  • getter 獲取 value 屬性時(shí),trace 收集依賴
  • setter 設(shè)置 value 屬性時(shí),trigger 觸發(fā)依賴

因此,只有訪問/修改 ref 的 value 屬性,才會收集/觸發(fā)依賴

依賴是怎么被收集的

export function trackRefValue(ref: RefBase<any>) {
// 判斷是否需要收集依賴
if (isTracking()) {
ref = toRaw(ref)
// 如果沒有 dep 屬性,則初始化 dep,dep 是一個(gè) Set<ReactiveEffect>,存儲副作用函數(shù)
if (!ref.dep) {
ref.dep = createDep()
}
// 收集 effect 依賴
trackEffects(ref.dep)
}
}
// 判斷是否需要收集依賴
export function isTracking() {
// shouldTrack 是一個(gè)全局變量,代表當(dāng)前是否需要 track 收集依賴
// activeEffect 也是個(gè)全局變量,代表當(dāng)前的副作用對象 ReactiveEffect
return shouldTrack && activeEffect !== undefined
}
復(fù)制代碼

 為什么需要使用 isTracking,來判斷是否收集依賴?

不是任何情況 ref 被訪問時(shí),都需要收集依賴。例如:

  • 沒有被 effect 包裹時(shí),由于沒有副作用函數(shù)(即沒有依賴,activeEffect === undefined),不應(yīng)該收集依賴
  • 某些特殊情況,即使包裹在 effect,也不應(yīng)該收集依賴(即 shouldTrack === false)。如:組件生命周期執(zhí)行、組件 setup 執(zhí)行

 ref.dep 有什么作用?

ref.dep 的類型是Set<ReactiveEffect> ,關(guān)于 ReactiveEffect 的細(xì)節(jié)會在后面詳細(xì)闡述

ref.dep 用于存儲副作用對象,這些副作用對象,依賴該 ref,ref 被修改時(shí)就會觸發(fā)

我們再來看看 trackEffects:

// 代表當(dāng)前的副作用 effect
let activeEffect: ReactiveEffect | undefined
export function trackEffects(
dep: Dep
) {
// 這個(gè)是局部變量的 shouldTrack,跟上一部分的全局 shouldTrack 不一樣
let shouldTrack = false
// 已經(jīng) track 收集過依賴,就可以跳過了
shouldTrack = !dep.has(activeEffect!)
if (shouldTrack) {
// 收集依賴,將 effect 存儲到 dep
dep.add(activeEffect!)
// 同時(shí) effect 也記錄一下 dep
// 用于 trigger 觸發(fā) effect 后,刪除 dep 里面對應(yīng)的 effect,即 dep.delete(activeEffect)
activeEffect!.deps.push(dep)
}
}
復(fù)制代碼

收集依賴,就是把 activeEffect(當(dāng)前的副作用對象),保存到 ref.dep 中(當(dāng)觸發(fā)依賴時(shí),遍歷 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 對象的某個(gè)屬性的值
// 這時(shí)候在 triggerRefValue(this, newVal) 時(shí)取 this,拿到的是一個(gè) reactive 對象
// 需要獲取 Proxy 代理背后的真實(shí)值 ref 對象
ref = toRaw(ref)
// 有依賴才觸發(fā) effect
if (ref.dep) {
triggerEffects(ref.dep)
}
}
復(fù)制代碼

再來看看 triggerEffects

export function triggerEffects(
dep: Dep | ReactiveEffect[]
) {
// 循環(huán)遍歷 dep,去取每個(gè)依賴的副作用對象 ReactiveEffect
for (const effect of isArray(dep) ? dep : [...dep]) {
// 默認(rèn)不允許遞歸,即當(dāng)前 effect 副作用函數(shù),如果遞歸觸發(fā)當(dāng)前 effect,會被忽略
if (effect !== activeEffect || effect.allowRecurse) {
// effect.scheduler可以先不管,ref 和 reactive 都沒有
if (effect.scheduler) {
effect.scheduler()
} else {
// 執(zhí)行 effect 的副作用函數(shù)
effect.run()
}
}
}
}
復(fù)制代碼

這里省略了一些代碼,這樣結(jié)構(gòu)更清晰。

當(dāng) ref 被修改時(shí),會 trigger 觸發(fā)依賴,即執(zhí)行了 ref.dep 里的所有副作用函數(shù)(effect.run 運(yùn)行副作用函數(shù))

為什么默認(rèn)不允許遞歸?

const foo = ref([])
effect(()=>{
foo.value.push(1)
})
復(fù)制代碼

在這個(gè)副作用函數(shù)中,即會使用到 foo.value(getter 收集依賴),又會修改 foo 數(shù)組(觸發(fā)依賴)。如果允許遞歸,會無限循環(huán)。

至此,ref 依賴收集和觸發(fā)的邏輯,已經(jīng)比較清晰了。

那么,接下來,我們需要進(jìn)一步了解的是,effect 函數(shù)、ReactiveEffect 副作用對象、副作用函數(shù),它們是什么,它們之間有什么關(guān)系?

effect 函數(shù)

我們來看一下 effect 的實(shí)現(xiàn)

// 傳入一個(gè) fn 函數(shù)
export function effect<T = any>(
fn: () => T
){
// 參數(shù) fn,可能也是一個(gè) 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ù)制代碼

effect 函數(shù)接受一個(gè)函數(shù)作為參數(shù),該函數(shù),我們稱之為副作用函數(shù)

effect 函數(shù)內(nèi)部,會創(chuàng)建 ReactiveEffect 對象,我們稱之為副作用對象

effect 函數(shù),返回一個(gè) runner,是一個(gè)函數(shù),直接調(diào)用就是調(diào)用副作用函數(shù);runner 的屬性 effect,保存著它對應(yīng)的 ReactiveEffect 對象 。

因此,它們的關(guān)系如下:

effect 函數(shù)的入?yún)楦弊饔煤瘮?shù),在 effect 函數(shù)內(nèi)部會創(chuàng)建副作用對象

我們繼續(xù)深入看看 ReactiveEffect 對象的實(shí)現(xiàn)

ReactiveEffect 副作用對象

該部分(effect.run 函數(shù))代碼有比較大的刪減,點(diǎn)擊查看未刪減的源碼[4]

為什么要刪減這部分代碼?

在 vue 3.2 版本以后,effect.run 做了優(yōu)化,提升性能,其中涉及到位運(yùn)算。

優(yōu)化方案在極端的情況下(effect 非常多次嵌套),會降級到原來的老方案(優(yōu)化前,3.2版本前的方案)

因此,為了便于理解,我這里先介紹優(yōu)化前的方案,深入了解,并闡述該方案的缺點(diǎn), 以便更好地理解為什么需要進(jìn)行優(yōu)化。

刪減部分為優(yōu)化后的方案,這部分的方案會在下一小節(jié)進(jìn)行介紹。

下面是 ReactiveEffect 代碼解析:

// 全局公用的 effect 棧,由于可以 effect 嵌套,因此需要用棧保存 ReactiveEffect 副作用對象
const effectStack: ReactiveEffect[] = []
export class ReactiveEffect<T = any> {
active = true
// 存儲 Dep 對象,如上一小節(jié)的 ref.dep
deps: Dep[] = []
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope | null
) {
// 可以暫時(shí)不看,與 effectScope API 相關(guān) https://v3.cn.vuejs.org/api/effect-scope.html#effectscope
// 將當(dāng)前 ReactiveEffect 副作用對象,記錄到 effectScope 中
// 當(dāng) effectScope.stop() 被調(diào)用時(shí),所有的 ReactiveEffect 對象都會被 stop
recordEffectScope(this, scope)
}
run() {
// 如果當(dāng)前 ReactiveEffect 副作用對象,已經(jīng)在棧里了,就不需要再處理了
if (!effectStack.includes(this)) {
try {
// 保存上一個(gè)的 activeEffect,因?yàn)?effect 可以嵌套
effectStack.push((activeEffect = this))
// 開啟 shouldTrack 開關(guān),緩存上一個(gè)值
enableTracking()
// 在該 effect 所在的所有 dep 中,清除 effect,下面會詳細(xì)闡述
cleanupEffect(this)
// 執(zhí)行副作用函數(shù),執(zhí)行過程中,又會 track 當(dāng)前的 effect 進(jìn)來,依賴重新被收集
return this.fn()
} finally {
// 關(guān)閉shouldTrack開關(guān),恢復(fù)上一個(gè)值
resetTracking()
// 恢復(fù)上一個(gè)的 activeEffect
effectStack.pop()
const n = effectStack.length
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
}
}
}
// 允許 track
export function enableTracking() {
// trackStack 是個(gè)全局的棧,由于 effect 可以嵌套,所以是否 track 的標(biāo)記,也需要用棧保存
trackStack.push(shouldTrack)
// 打開全局 shouldTrack 開關(guān)
shouldTrack = true
}
// 重置上一個(gè) track 狀態(tài)
export function resetTracking() {
const last = trackStack.pop()
// 恢復(fù)上一個(gè) track 狀態(tài)
shouldTrack = last === undefined ? true : last
}
復(fù)制代碼

為什么要用棧保存 effect 和 track 狀態(tài)?

因?yàn)閑ffect可能會嵌套,需要保存之前的狀態(tài),effect執(zhí)行完成后恢復(fù)

cleanupEffect 做了什么?

回顧下圖:

image-20220102234627353

effect.deps,也存儲著響應(yīng)式變量的 dep(dep 是一個(gè)依賴集合, ReactiveEffect 對象的集合),目的是在effect 執(zhí)行后,在所有的 dep 中刪除當(dāng)前執(zhí)行過的 effect,雙向刪除

刪除代碼如下:

function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
// 從 ref.dep 中刪除 ReactiveEffect
deps[i].delete(effect)
}
// 從 ReactiveEffect.deps 中刪除 dep
deps.length = 0
}
}
復(fù)制代碼

刪除的 ReactiveEffect 如何被重新收集?

在 cleanupEffect 中,在各個(gè) dep 中,刪除該 ReactiveEffect 對象。

在執(zhí)行 this.fn() 時(shí),執(zhí)行副作用函數(shù),副作用函數(shù)的執(zhí)行中,當(dāng)使用到響應(yīng)式變量(如 ref.value)時(shí),又會 trackEffect,重新收集依賴。

為什么要先刪除,再重新收集依賴?

因?yàn)閳?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
復(fù)制代碼

當(dāng) switch 為 true 時(shí),triggerEffect,雙向刪除后,執(zhí)行副作用函數(shù),switch、foo 會重新收集到依賴 effect

當(dāng) switch 變成 false 后,triggerEffect,雙向刪除后,執(zhí)行副作用函數(shù),僅有 switch 能重新收集到依賴 effect

image-20211231110604009

由于 effect 副作用函數(shù)執(zhí)行前后,依賴的響應(yīng)式變量(這里是 ref )可能不一致,因此 vue 會先刪除全部依賴,再重新收集。

細(xì)心的你,可能會發(fā)現(xiàn):自己寫 vue 代碼時(shí),很少會出現(xiàn)前后依賴不一致的情況。那既然這樣,刪除全部依賴這個(gè)實(shí)現(xiàn)就有優(yōu)化的空間,能不能只刪除失效的依賴呢?

依賴更新算法優(yōu)化

該優(yōu)化是 vue 3.2 版本引入的,原因即上一小節(jié)所說的,可以只刪除失效的依賴。并且在極端的嵌套深度下,能夠降級到 cleanupEffect 方法,對所有依賴進(jìn)行刪除。

先想想,假如是自己實(shí)現(xiàn),要怎么寫好呢?

  1. 不使用 cleanupEffect 刪除所有依賴
  2. 執(zhí)行副作用函數(shù)前,給 ReactiveEffect 依賴的響應(yīng)式變量,加上 was 的標(biāo)記(was 是 vue 給的名稱,過去的意思)
  3. 執(zhí)行 this.fn(),track 重新收集依賴時(shí),給 ReactiveEffect 的每個(gè)依賴,加上 new 的標(biāo)記
  4. 最后,對失效(有 was 但是沒有 new)依賴進(jìn)行刪除

 為什么是標(biāo)記在響應(yīng)式對象,而不是 ReactiveEffect ?

再回顧一下響應(yīng)式變量和 ReactiveEffect 的關(guān)系:

image-20211231112331231

ReactiveEffect 依賴響應(yīng)式變量(ref),響應(yīng)式變量(ref)擁有多個(gè) ReactiveEffect 依賴。

只刪除失效的依賴。就要確定哪些依賴(響應(yīng)式變量)需要被刪除(實(shí)際上是響應(yīng)式變量的 dep 被刪除)

因此,需要在響應(yīng)式變量上做標(biāo)記,對已經(jīng)不依賴的響應(yīng)式變量,將它們的 dep,從 ReactiveEffect.deps 中刪除

如何給響應(yīng)式變量做標(biāo)記

實(shí)現(xiàn)如下:

export const initDepMarkers = ({ deps }: ReactiveEffect) => {
if (deps.length) {
// 循環(huán) deps,對每個(gè) dep 進(jìn)行標(biāo)記
for (let i = 0; i < deps.length; i++) {
// 標(biāo)記 dep 為 was,w 是 was 的意思
deps[i].w |= trackOpBit
}
}
}
復(fù)制代碼

這部分代碼其實(shí)比較難理解,尤其是使用了位運(yùn)算符,如果一開始就解析這些代碼的話,很容易就勸退了。

下面我們對問題進(jìn)行分析:

為什么這里標(biāo)記的是 dep?

這里的 dep,對于 ref,就是 ref.dep,它是一個(gè) Set<ReactiveEffect> 。

dep 跟 ref 的關(guān)系是一一對應(yīng)的,一個(gè) ref僅僅有一個(gè) dep,因此,標(biāo)記在 dep 和 標(biāo)記在 ref,是等價(jià)的

那為什么不在響應(yīng)式變量上標(biāo)記呢?

因?yàn)轫憫?yīng)式變量的類型有幾種:ref、computed、reactive,它們都使用 dep 對象存儲依賴,對它們都有的 dep 對象進(jìn)行標(biāo)記,可以將標(biāo)記代碼更好的進(jìn)行復(fù)用(否則要判斷不同的類型,執(zhí)行不同的標(biāo)記邏輯)。

如果未來新增一種響應(yīng)式變量,只需要也是用 dep 進(jìn)行存儲依賴即可

這個(gè)按位與位運(yùn)算的作用是什么?

先來看看 dep 的真實(shí)結(jié)構(gòu),它其實(shí)還有兩個(gè)屬性 w 和 n:

export type Dep = Set<ReactiveEffect> & TrackedMarkers
type TrackedMarkers = {
/**
* wasTracked,代表副作用函數(shù)執(zhí)行前被 track 過
*/
w: number
/**
* newTracked,代表副作用函數(shù)執(zhí)行后被 track
*/
n: number
}
復(fù)制代碼

那這個(gè) w 和 n 是怎么做標(biāo)記的?我們先來看看位運(yùn)算做了什么,不了解位運(yùn)算的同學(xué) ,可以先看看這里的介紹[5]

dep.w |= trackOpBit // 即 dep.w = dep.w | trackOpBit
復(fù)制代碼

image-20220103205852303

將響應(yīng)式變量標(biāo)記,就是將對應(yīng)整數(shù)的二進(jìn)制位,設(shè)置成 1

dep.n 的標(biāo)記方法也是如此。

為什么要使用位運(yùn)算?

  1. 位運(yùn)算速度快
  2. 只需要使用一個(gè) number 類型的數(shù)據(jù),就能存儲不同深度的標(biāo)記(was / new)

如果不使用位運(yùn)算,需要實(shí)現(xiàn)同樣的標(biāo)記能力,需要用數(shù)組存儲不同深度的標(biāo)記,數(shù)據(jù)結(jié)構(gòu)如下:

export type Dep = Set<ReactiveEffect> & TrackedMarkers
type TrackedMarkers = {
/**
* wasTrackedList,代表副作用函數(shù)執(zhí)行前被 track 過
* 設(shè)計(jì)為數(shù)組,是因?yàn)?effect 可以嵌套,代表響應(yīng)式變量在所在的 effect 深度(嵌套層級)中是否被 track
*/
wasTrackedList: boolean[]
/**
* newTracked,代表副作用函數(shù)執(zhí)行后被 track
* 設(shè)計(jì)為數(shù)組,是因?yàn)?effect 可以嵌套,代表響應(yīng)式變量在所在的 effect 深度(嵌套層級)中是否被 track
*/
newTrackedList: boolean[]
}
復(fù)制代碼

使用數(shù)組存儲標(biāo)記位,修改處理沒有直接位運(yùn)算快。由于 vue 每次執(zhí)行副作用函數(shù)(一個(gè)頁面有非常多的副作用函數(shù)),都需要頻繁進(jìn)行標(biāo)記,這開銷也是非常大的。因此,這里使用了運(yùn)算符,提升了標(biāo)記的速度,也節(jié)省了運(yùn)行內(nèi)存

trackOpBit 是什么?

trackOpBit 是代表當(dāng)前操作的位,它是由 effect 嵌套深度決定的。

// 全局變量嵌套深度一開始為 0  
effectTrackDepth = 0
// 每次執(zhí)行 effect 副作用函數(shù)前,全局變量嵌套深度會自增 1,執(zhí)行完成 effect 副作用函數(shù)后會自減
trackOpBit = 1 << ++effectTrackDepth
復(fù)制代碼

當(dāng)深度為 1 時(shí),trackOpBit 是 2(二進(jìn)制:00000010),操作的是第二位,將 dep.w 的第二位變成 1

因此如圖所說,dep.w 的第一位是不使用的

為什么最大標(biāo)記嵌套深度為 30?

從圖中我們可以看到,深度受存儲類型的位數(shù)限制,否則就會溢出。

在JavaScript內(nèi)部,數(shù)值都是以64位浮點(diǎn)數(shù)的形式儲存,但是做位運(yùn)算的時(shí)候,是以32位帶符號的整數(shù)進(jìn)行運(yùn)算的,并且返回值也是一個(gè)32位帶符號的整數(shù)。

1 << 30
// 1073741824
1 << 31
// -2147483648,溢出
復(fù)制代碼

因此,深度最大為 30,超過 30,則需要降級方案,使用全部清除再全部重新收集依賴的方案

判斷響應(yīng)式變量是否被標(biāo)記

export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
復(fù)制代碼

使用 wasTracked 和 newTracked 判斷 dep 是否在當(dāng)前深度被標(biāo)記

trackOpBit 是一個(gè)全局變量,根據(jù)當(dāng)前深度生成的

image-20220103210036377

如圖,如果需要判斷深度為 2 時(shí)(trackOpBit 第 3 位為 1),是否被標(biāo)記,僅當(dāng) dep.w 的第 3 位為 1 時(shí), wasTracked 或 newTracked 才會返回 true

vue 通過這樣巧妙的位運(yùn)算,快速算出依賴在當(dāng)前深度是否被標(biāo)記

副作用對象的優(yōu)化實(shí)現(xiàn)

// 當(dāng)前 effect 的嵌套深度,每次執(zhí)行會 ++effectTrackDepth
let effectTrackDepth = 0
// 最大的 effect 嵌套層數(shù)為 30
const maxMarkerBits = 30
// 位運(yùn)算操作的第 trackOpBit 位
export let trackOpBit = 1
export class ReactiveEffect<T = any> {
run() {
if (!effectStack.includes(this)) {
try {
// 省略代碼: 保存上一個(gè) activeEffect
// trackOpBit: 根據(jù)深度生成 trackOpBit
trackOpBit = 1 << ++effectTrackDepth
// maxMarkerBits: 可支持的最大嵌套深度,為 30
// 這里就是之前說到的,正常情況下使用優(yōu)化方案,極端嵌套場景下,使用降級方案
if (effectTrackDepth <= maxMarkerBits) {
// 標(biāo)記所有的 dep 為 was
initDepMarkers(this)
} else {
// 降級方案,刪除所有的依賴,再重新收集
cleanupEffect(this)
}
// 執(zhí)行過程中標(biāo)記新的 dep 為 new
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
// 對失效依賴進(jìn)行刪除
finalizeDepMarkers(this)
}
// 恢復(fù)上一次的狀態(tài)
// 嵌套深度 effectTrackDepth 自減
// 重置操作的位數(shù)
trackOpBit = 1 << --effectTrackDepth
// 省略代碼: 恢復(fù)上一個(gè) activeEffect
}
}
}
}
復(fù)制代碼

整體的思路如下:

  •    如果當(dāng)前深度不超過 30,使用優(yōu)化方案

        1.執(zhí)行副作用函數(shù)前,給 ReactiveEffect 依賴的響應(yīng)式變量,加上 was 的標(biāo)記(was 是 vue 給的名稱,表示過去依賴)

        2.對失效依賴進(jìn)行刪除(有 was 但是沒有 new)

        3.恢復(fù)上一個(gè)深度的狀態(tài)

  •    如果深度超過 30 ,超過部分,使用降級方案:

        1.雙向刪除 ReactiveEffect 副作用對象的所有依賴(effect.deps.length = 0)

        2.執(zhí)行 this.fn(),track 重新收集依賴時(shí)

        3.恢復(fù)上一個(gè)深度的狀態(tài)

標(biāo)記 ReactiveEffect 的所有的 dep 為 was 的實(shí)現(xiàn):

export const initDepMarkers = ({ deps }: ReactiveEffect) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit // 遍歷每個(gè) dep 標(biāo)記為 was
}
}
}
復(fù)制代碼

對失效依賴進(jìn)行刪除的實(shí)現(xiàn)如下(有 was 但是沒有 new):

vue 通過這樣巧妙的位運(yùn)算,快速算出依賴在當(dāng)前深度是否被標(biāo)記
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
//有 was 標(biāo)記但是沒有 new 標(biāo)記,應(yīng)當(dāng)刪除
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect)
} else {
// 需要保留的依賴,放到數(shù)據(jù)的較前位置,因?yàn)樵谧詈髸h除較后位置的所有依賴
deps[ptr++] = dep
}
// 清理 was 和 new 標(biāo)記,將它們對應(yīng)深度的 bit,置為 0
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
// 刪除依賴,只保留需要的
deps.length = ptr
}
}
復(fù)制代碼

參考文章

  • vue 官方文檔[6]
  •  vue-next 源碼[7]

最后

如果這篇文章對您有所幫助,請幫忙點(diǎn)個(gè)贊??,您的鼓勵是我創(chuàng)作路上的最大的動力。

責(zé)任編輯:龐桂玉 來源: 前端教程
相關(guān)推薦

2021-09-27 06:29:47

Vue3 響應(yīng)式原理Vue應(yīng)用

2021-12-02 05:50:35

Vue3 插件Vue應(yīng)用

2021-09-22 07:57:23

Vue3 插件Vue應(yīng)用

2022-06-26 00:00:02

Vue3響應(yīng)式系統(tǒng)

2022-06-21 12:09:18

Vue差異

2024-07-08 08:43:19

2023-12-06 07:43:56

Vue如何定義事件

2020-06-09 11:35:30

Vue 3響應(yīng)式前端

2023-02-06 08:39:01

PreactVue3響應(yīng)式

2024-08-13 09:26:07

2024-03-21 08:34:49

Vue3WebSocketHTTP

2020-03-12 09:02:34

數(shù)據(jù)思維統(tǒng)計(jì)學(xué)大數(shù)據(jù)

2025-02-17 08:58:06

2022-12-06 08:39:27

Vue3Reactive

2024-04-11 13:10:00

Vue3Reactive響應(yīng)性

2024-02-01 09:10:04

頁面引導(dǎo)工具Vue3

2023-11-28 09:03:59

Vue.jsJavaScript

2024-01-09 09:40:23

2022-03-10 11:04:04

Vue3Canvas前端

2024-03-22 08:57:04

Vue3Emoji表情符號
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號