<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 10193字,需瀏覽 21分鐘

           ·

          2022-01-24 12:10

          (給前端大學加星標,提升前端技能.)前言

          本文使用 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é)論:

          1. 被 effect 包裹的函數(shù),會自動執(zhí)行一次。

          2. 被 effect 函數(shù)包裹的函數(shù)體,擁有了響應性 —— 當 effect 內(nèi)的函數(shù)中的 ref 對象 a.value 被修改時,該函數(shù)會自動重新執(zhí)行。

          3. 當 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 依賴
          da452b6640a427ef13727c6b5fdb7183.webpimage-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),雙向刪除依賴)

          ccf60864ee1cadec23934c8cdce7fed7.webpimage-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 做了什么?

          回顧下圖:

          8cca4a509843aa436f46a9722e0d9388.webpimage-20220102234627353

          effect.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

          de5a799fc425e949f61b76b09446ec89.webpimage-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),要怎么寫好呢?

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

          為什么是標記在響應式對象,而不是 ReactiveEffect ?

          再回顧一下響應式變量和 ReactiveEffect 的關系:

          da452b6640a427ef13727c6b5fdb7183.webpimage-20211231112331231

          ReactiveEffect 依賴響應式變量(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
          復制代碼
          e2f5b12880fc8ef7c4aa3e692b7814d9.webpimage-20220103205852303

          將響應式變量標記,就是將對應整數(shù)的二進制位,設置成 1

          dep.n 的標記方法也是如此。

          為什么要使用位運算?

          1. 位運算速度快
          2. 只需要使用一個 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?<復制代碼

          當深度為 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
          復制代碼

          使用 wasTrackednewTracked 判斷 dep 是否在當前深度被標記

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

          b9c9d55bcda9cbb9d2ed42a93ecaf093.webpimage-20220103210036377

          如圖,如果需要判斷深度為 2 時(trackOpBit 第 3 位為 1),是否被標記,僅當 dep.w 的第 3 位為 1 時, wasTrackednewTracked 才會返回 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?<
          ????????//?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?<
          ????????//?省略代碼:?恢復上一個?activeEffect
          ??????}
          ????}
          ??}
          }
          復制代碼

          整體的思路如下:

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

          1. 執(zhí)行副作用函數(shù)前,給 ReactiveEffect 依賴的響應式變量,加上 was 的標記(was 是 vue 給的名稱,表示過去依賴)
          2. 執(zhí)行 this.fn(),track 重新收集依賴時,給 ReactiveEffect 的每個依賴,加上 new 的標記
          3. 對失效依賴進行刪除(有 was 但是沒有 new)
          4. 恢復上一個深度的狀態(tài)

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

          1. 雙向刪除 ReactiveEffect 副作用對象的所有依賴(effect.deps.length = 0)
          2. 執(zhí)行 this.fn()track 重新收集依賴時
          3. 恢復上一個深度的狀態(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):

          424deba1e3b16f21c83b756539ba40aa.webp
          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]
          最后如果這篇文章對您有所幫助,請幫忙點個贊??,您的鼓勵是我創(chuàng)作路上的最大的動力。作者:candyTonghttps://juejin.cn/post/7048970987500470279點贊和在看就是最大的支持??
          瀏覽 32
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产成人黄色在线视频 | 豆花视频在线资源站 | 六月婷婷五月 | 巨乳波霸在线 | 免费无码做爱视频 |