细说 Vue.js 3.2 关于响应式部分的优化
大家好,我是若川。上一篇寫(xiě)的是:初學(xué)者也能看懂的 Vue3 源碼中那些實(shí)用的基礎(chǔ)工具函數(shù)。今天再分享一篇?Vue 3.2?的文章。
學(xué)習(xí)源碼整體架構(gòu)系列、年度總結(jié)、JS基礎(chǔ)系列
背景
Vue 3 正式發(fā)布距今已經(jīng)快一年了,相信很多小伙伴已經(jīng)在生產(chǎn)環(huán)境用上了 Vue 3 了。如今,Vue.js 3.2 已經(jīng)正式發(fā)布,而這次 minor 版本的升級(jí)主要體現(xiàn)在源碼層級(jí)的優(yōu)化,對(duì)于用戶(hù)的使用層面來(lái)說(shuō)其實(shí)變化并不大。其中一個(gè)吸引我的點(diǎn)是提升了響應(yīng)式的性能:
More efficient ref implementation (~260% faster read / ~50% faster write)
~40% faster dependency tracking
~17% less memory usage
翻譯過(guò)來(lái)就是 ref API 的讀效率提升約為 260%,寫(xiě)效率提升約為 50% ,依賴(lài)收集的效率提升約為 40%,同時(shí)還減少了約 17% 的內(nèi)存使用。
這簡(jiǎn)直就是一個(gè)吊炸天的優(yōu)化啊,因?yàn)橐理憫?yīng)式系統(tǒng)是 Vue.js 的核心實(shí)現(xiàn)之一,對(duì)它的優(yōu)化就意味著對(duì)所有使用 Vue.js 開(kāi)發(fā)的 App 的性能優(yōu)化。
而且這個(gè)優(yōu)化并不是 Vue 官方人員實(shí)現(xiàn)的,而是社區(qū)一位大佬 @basvanmeurs 提出的,相關(guān)的優(yōu)化代碼在 2020 年 10 月 9 號(hào)就已經(jīng)提交了,但由于對(duì)內(nèi)部的實(shí)現(xiàn)改動(dòng)較大,官方一直等到了 Vue.js 3.2 發(fā)布,才把代碼合入。
這次 basvanmeurs 提出的響應(yīng)式性能優(yōu)化真的讓尤大喜出望外,不僅僅是大大提升了 Vue 3 的運(yùn)行時(shí)性能,還因?yàn)檫@么核心的代碼能來(lái)自社區(qū)的貢獻(xiàn),這就意味著 Vue 3 受到越來(lái)越多的人關(guān)注;一些能力強(qiáng)的開(kāi)發(fā)人員參與到核心代碼的貢獻(xiàn),可以讓 Vue 3 走的更遠(yuǎn)更好。
我們知道,相比于 Vue 2,Vue 3 做了多方面的優(yōu)化,其中一部分是數(shù)據(jù)響應(yīng)式的實(shí)現(xiàn)由 Object.defineProperty API 改成了 Proxy API。
當(dāng)初 Vue 3 在宣傳的時(shí)候,官方宣稱(chēng)在響應(yīng)式實(shí)現(xiàn)的性能上做了優(yōu)化,那么優(yōu)化體現(xiàn)在哪些方面呢?有部分小伙伴認(rèn)為是 Proxy API 的性能要優(yōu)于 Object.defineProperty 的,其實(shí)不然,實(shí)際上 Proxy 在性能上是要比 Object.defineProperty 差的,詳情可以參考 Thoughts on ES6 Proxies Performance 這篇文章,而我也對(duì)此做了測(cè)試,結(jié)論同上,可以參考這個(gè) repo。
既然 Proxy 慢,為啥 Vue 3 還是選擇了它來(lái)實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式呢?因?yàn)?Proxy 本質(zhì)上是對(duì)某個(gè)對(duì)象的劫持,這樣它不僅僅可以監(jiān)聽(tīng)對(duì)象某個(gè)屬性值的變化,還可以監(jiān)聽(tīng)對(duì)象屬性的新增和刪除;而 Object.defineProperty 是給對(duì)象的某個(gè)已存在的屬性添加對(duì)應(yīng)的 getter 和 setter,所以它只能監(jiān)聽(tīng)這個(gè)屬性值的變化,而不能去監(jiān)聽(tīng)對(duì)象屬性的新增和刪除。
而響應(yīng)式在性能方面的優(yōu)化其實(shí)是體現(xiàn)在把嵌套層級(jí)較深的對(duì)象變成響應(yīng)式的場(chǎng)景。在 Vue 2 的實(shí)現(xiàn)中,在組件初始化階段把數(shù)據(jù)變成響應(yīng)式時(shí),遇到子屬性仍然是對(duì)象的情況,會(huì)遞歸執(zhí)行 Object.defineProperty 定義子對(duì)象的響應(yīng)式;而在 Vue 3 的實(shí)現(xiàn)中,只有在對(duì)象屬性被訪(fǎng)問(wèn)的時(shí)候才會(huì)判斷子屬性的類(lèi)型來(lái)決定要不要遞歸執(zhí)行 reactive,這其實(shí)是一種延時(shí)定義子對(duì)象響應(yīng)式的實(shí)現(xiàn),在性能上會(huì)有一定的提升。
因此,相比于 Vue 2,Vue 3 確實(shí)在響應(yīng)式實(shí)現(xiàn)部分做了一定的優(yōu)化,但實(shí)際上效果是有限的。而 Vue.js 3.2 這次在響應(yīng)式性能方面的優(yōu)化,是真的做到了質(zhì)的飛躍,接下來(lái)我們就來(lái)上點(diǎn)硬菜,從源碼層面分析具體做了哪些優(yōu)化,以及這些優(yōu)化背后帶來(lái)的技術(shù)層面的思考。
響應(yīng)式實(shí)現(xiàn)原理
所謂響應(yīng)式,就是當(dāng)我們修改數(shù)據(jù)后,可以自動(dòng)做某些事情;對(duì)應(yīng)到組件的渲染,就是修改數(shù)據(jù)后,能自動(dòng)觸發(fā)組件的重新渲染。
Vue 3 實(shí)現(xiàn)響應(yīng)式,本質(zhì)上是通過(guò) Proxy API 劫持了數(shù)據(jù)對(duì)象的讀寫(xiě),當(dāng)我們?cè)L問(wèn)數(shù)據(jù)時(shí),會(huì)觸發(fā) getter 執(zhí)行依賴(lài)收集;修改數(shù)據(jù)時(shí),會(huì)觸發(fā) setter 派發(fā)通知。
接下來(lái),我們簡(jiǎn)單分析一下依賴(lài)收集和派發(fā)通知的實(shí)現(xiàn)(Vue.js 3.2 之前的版本)。
依賴(lài)收集
首先來(lái)看依賴(lài)收集的過(guò)程,核心就是在訪(fǎng)問(wèn)響應(yīng)式數(shù)據(jù)的時(shí)候,觸發(fā) getter 函數(shù),進(jìn)而執(zhí)行 track 函數(shù)收集依賴(lài):
let?shouldTrack?=?true //?當(dāng)前激活的?effect let?activeEffect //?原始數(shù)據(jù)對(duì)象?map const?targetMap?=?new?WeakMap() function?track(target,?type,?key)?{if?(!shouldTrack?||?activeEffect?===?undefined)?{return}let?depsMap?=?targetMap.get(target)if?(!depsMap)?{//?每個(gè)?target?對(duì)應(yīng)一個(gè)?depsMaptargetMap.set(target,?(depsMap?=?new?Map()))}let?dep?=?depsMap.get(key)if?(!dep)?{//?每個(gè)?key?對(duì)應(yīng)一個(gè)?dep?集合depsMap.set(key,?(dep?=?new?Set()))}if?(!dep.has(activeEffect))?{//?收集當(dāng)前激活的?effect?作為依賴(lài)dep.add(activeEffect)//?當(dāng)前激活的?effect?收集?dep?集合作為依賴(lài)activeEffect.deps.push(dep)} }分析這個(gè)函數(shù)的實(shí)現(xiàn)前,我們先想一下要收集的依賴(lài)是什么,我們的目的是實(shí)現(xiàn)響應(yīng)式,就是當(dāng)數(shù)據(jù)變化的時(shí)候可以自動(dòng)做一些事情,比如執(zhí)行某些函數(shù),所以我們收集的依賴(lài)就是數(shù)據(jù)變化后執(zhí)行的副作用函數(shù)。
track 函數(shù)擁有三個(gè)參數(shù),其中 target 表示原始數(shù)據(jù);type 表示這次依賴(lài)收集的類(lèi)型;key 表示訪(fǎng)問(wèn)的屬性。
track 函數(shù)外部創(chuàng)建了全局的 targetMap 作為原始數(shù)據(jù)對(duì)象的 Map,它的鍵是 target,值是 depsMap,作為依賴(lài)的 Map;這個(gè) depsMap 的鍵是 target 的 key,值是 dep 集合,dep 集合中存儲(chǔ)的是依賴(lài)的副作用函數(shù)。為了方便理解,可以通過(guò)下圖表示它們之間的關(guān)系:
因此每次執(zhí)行 track 函數(shù),就是把當(dāng)前激活的副作用函數(shù) activeEffect 作為依賴(lài),然后收集到 target 相關(guān)的 depsMap 對(duì)應(yīng) key 下的依賴(lài)集合 dep 中。
派發(fā)通知
派發(fā)通知發(fā)生在數(shù)據(jù)更新的階段,核心就是在修改響應(yīng)式數(shù)據(jù)時(shí),觸發(fā) setter 函數(shù),進(jìn)而執(zhí)行 trigger 函數(shù)派發(fā)通知:
const?targetMap?=?new?WeakMap() function?trigger(target,?type,?key)?{//?通過(guò)?targetMap?拿到?target?對(duì)應(yīng)的依賴(lài)集合const?depsMap?=?targetMap.get(target)if?(!depsMap)?{//?沒(méi)有依賴(lài),直接返回return}//?創(chuàng)建運(yùn)行的?effects?集合const?effects?=?new?Set()//?添加?effects?的函數(shù)const?add?=?(effectsToAdd)?=>?{if?(effectsToAdd)?{effectsToAdd.forEach(effect?=>?{effects.add(effect)})}}//?SET?|?ADD?|?DELETE?操作之一,添加對(duì)應(yīng)的?effectsif?(key?!==?void?0)?{add(depsMap.get(key))}const?run?=?(effect)?=>?{//?調(diào)度執(zhí)行if?(effect.options.scheduler)?{effect.options.scheduler(effect)}else?{//?直接運(yùn)行effect()}}//?遍歷執(zhí)行?effectseffects.forEach(run) }trigger 函數(shù)擁有三個(gè)參數(shù),其中 target 表示目標(biāo)原始對(duì)象;type 表示更新的類(lèi)型;key 表示要修改的屬性。
trigger 函數(shù) 主要做了四件事情:
從 targetMap 中拿到 target 對(duì)應(yīng)的依賴(lài)集合 depsMap;
創(chuàng)建運(yùn)行的 effects 集合;
根據(jù) key 從 depsMap 中找到對(duì)應(yīng)的 effect 添加到 effects 集合;
遍歷 effects 執(zhí)行相關(guān)的副作用函數(shù)。
因此每次執(zhí)行 trigger 函數(shù),就是根據(jù) target 和 key,從 targetMap 中找到相關(guān)的所有副作用函數(shù)遍歷執(zhí)行一遍。
在描述依賴(lài)收集和派發(fā)通知的過(guò)程中,我們都提到了一個(gè)詞:副作用函數(shù),依賴(lài)收集過(guò)程中我們把 activeEffect(當(dāng)前激活副作用函數(shù))作為依賴(lài)收集,它又是什么?接下來(lái)我們來(lái)看一下副作用函數(shù)的廬山真面目。
副作用函數(shù)
那么,什么是副作用函數(shù),在介紹它之前,我們先回顧一下響應(yīng)式的原始需求,即我們修改了數(shù)據(jù)就能自動(dòng)做某些事情,舉個(gè)簡(jiǎn)單的例子:
import?{?reactive?}?from?'vue' const?counter?=?reactive({num:?0 }) function?logCount()?{console.log(counter.num) } function?count()?{counter.num++ } logCount() count()我們定義了響應(yīng)式對(duì)象 counter,然后在 logCount 中訪(fǎng)問(wèn)了 counter.num,我們希望在執(zhí)行 count 函數(shù)修改 counter.num 值的時(shí)候,能自動(dòng)執(zhí)行 logCount 函數(shù)。
按我們之前對(duì)依賴(lài)收集過(guò)程的分析,如果logCount 是 activeEffect 的話(huà),那么就可以實(shí)現(xiàn)需求,但顯然是做不到的,因?yàn)榇a在執(zhí)行到 console.log(counter.num) 這一行的時(shí)候,它對(duì)自己在 logCount 函數(shù)中的運(yùn)行是一無(wú)所知的。
那么該怎么辦呢?其實(shí)只要我們運(yùn)行 logCount 函數(shù)前,把 logCount 賦值給 activeEffect 就好了:
activeEffect?=?logCount? logCount()順著這個(gè)思路,我們可以利用高階函數(shù)的思想,對(duì) logCount 做一層封裝:
function?wrapper(fn)?{const?wrapped?=?function(...args)?{activeEffect?=?fnfn(...args)}return?wrapped } const?wrappedLog?=?wrapper(logCount) wrappedLog()wrapper 本身也是一個(gè)函數(shù),它接受 fn 作為參數(shù),返回一個(gè)新的函數(shù) wrapped,然后維護(hù)一個(gè)全局變量 activeEffect,當(dāng) wrapped 執(zhí)行的時(shí)候,把 activeEffect 設(shè)置為 fn,然后執(zhí)行 fn 即可。
這樣當(dāng)我們執(zhí)行 wrappedLog 后,再去修改 counter.num,就會(huì)自動(dòng)執(zhí)行 logCount 函數(shù)了。
實(shí)際上 Vue 3 就是采用類(lèi)似的做法,在它內(nèi)部就有一個(gè) effect 副作用函數(shù),我們來(lái)看一下它的實(shí)現(xiàn):
//?全局?effect?棧 const?effectStack?=?[] //?當(dāng)前激活的?effect let?activeEffect function?effect(fn,?options?=?EMPTY_OBJ)?{if?(isEffect(fn))?{//?如果?fn?已經(jīng)是一個(gè)?effect?函數(shù)了,則指向原始函數(shù)fn?=?fn.raw}//?創(chuàng)建一個(gè)?wrapper,它是一個(gè)響應(yīng)式的副作用的函數(shù)const?effect?=?createReactiveEffect(fn,?options)if?(!options.lazy)?{//?lazy?配置,計(jì)算屬性會(huì)用到,非?lazy?則直接執(zhí)行一次effect()}return?effect } function?createReactiveEffect(fn,?options)?{const?effect?=?function?reactiveEffect()?{if?(!effect.active)?{//?非激活狀態(tài),則判斷如果非調(diào)度執(zhí)行,則直接執(zhí)行原始函數(shù)。return?options.scheduler???undefined?:?fn()}if?(!effectStack.includes(effect))?{//?清空?effect?引用的依賴(lài)cleanup(effect)try?{//?開(kāi)啟全局?shouldTrack,允許依賴(lài)收集enableTracking()//?壓棧effectStack.push(effect)activeEffect?=?effect//?執(zhí)行原始函數(shù)return?fn()}finally?{//?出棧effectStack.pop()//?恢復(fù)?shouldTrack?開(kāi)啟之前的狀態(tài)resetTracking()//?指向棧最后一個(gè)?effectactiveEffect?=?effectStack[effectStack.length?-?1]}}}effect.id?=?uid++//?標(biāo)識(shí)是一個(gè)?effect?函數(shù)effect._isEffect?=?true//?effect?自身的狀態(tài)effect.active?=?true//?包裝的原始函數(shù)effect.raw?=?fn//?effect?對(duì)應(yīng)的依賴(lài),雙向指針,依賴(lài)包含對(duì)?effect?的引用,effect?也包含對(duì)依賴(lài)的引用effect.deps?=?[]//?effect?的相關(guān)配置effect.options?=?optionsreturn?effect }結(jié)合上述代碼來(lái)看,effect 內(nèi)部通過(guò)執(zhí)行 createReactiveEffect 函數(shù)去創(chuàng)建一個(gè)新的 effect 函數(shù),為了和外部的 effect 函數(shù)區(qū)分,我們把它稱(chēng)作 reactiveEffect 函數(shù),并且還給它添加了一些額外屬性(我在注釋中都有標(biāo)明)。另外,effect 函數(shù)還支持傳入一個(gè)配置參數(shù)以支持更多的 feature,這里就不展開(kāi)了。
reactiveEffect 函數(shù)就是響應(yīng)式的副作用函數(shù),當(dāng)執(zhí)行 trigger 過(guò)程派發(fā)通知的時(shí)候,執(zhí)行的 effect 就是它。
按我們之前的分析,reactiveEffect 函數(shù)只需要做兩件事情:讓全局的 activeEffect 指向它, 然后執(zhí)行被包裝的原始函數(shù) fn。
但實(shí)際上它的實(shí)現(xiàn)要更復(fù)雜一些,首先它會(huì)判斷 effect 的狀態(tài)是否是 active,這其實(shí)是一種控制手段,允許在非 active 狀態(tài)且非調(diào)度執(zhí)行情況,則直接執(zhí)行原始函數(shù) fn 并返回。
接著判斷 effectStack 中是否包含 effect,如果沒(méi)有就把 effect 壓入棧內(nèi)。之前我們提到,只要設(shè)置 activeEffect = effect 即可,那么這里為什么要設(shè)計(jì)一個(gè)棧的結(jié)構(gòu)呢?
其實(shí)是考慮到以下這樣一個(gè)嵌套 effect 的場(chǎng)景:
import?{?reactive}?from?'vue'? import?{?effect?}?from?'@vue/reactivity'? const?counter?=?reactive({?num:?0,?num2:?0? })? function?logCount()?{?effect(logCount2)?console.log('num:',?counter.num)? }? function?count()?{?counter.num++? }? function?logCount2()?{?console.log('num2:',?counter.num2)? }? effect(logCount)? count()我們每次執(zhí)行 effect 函數(shù)時(shí),如果僅僅把 reactiveEffect 函數(shù)賦值給 activeEffect,那么針對(duì)這種嵌套場(chǎng)景,執(zhí)行完 effect(logCount2) 后,activeEffect 還是 effect(logCount2) 返回的 reactiveEffect 函數(shù),這樣后續(xù)訪(fǎng)問(wèn) counter.num 的時(shí)候,依賴(lài)收集對(duì)應(yīng)的 activeEffect 就不對(duì)了,此時(shí)我們外部執(zhí)行 count 函數(shù)修改 counter.num 后執(zhí)行的便不是 logCount 函數(shù),而是 logCount2 函數(shù),最終輸出的結(jié)果如下:
num2:?0? num:?0? num2:?0而我們期望的結(jié)果應(yīng)該如下:
num2:?0? num:?0? num2:?0? num:?1因此針對(duì)嵌套 effect 的場(chǎng)景,我們不能簡(jiǎn)單地賦值 activeEffect,應(yīng)該考慮到函數(shù)的執(zhí)行本身就是一種入棧出棧操作,因此我們也可以設(shè)計(jì)一個(gè) effectStack,這樣每次進(jìn)入 reactiveEffect 函數(shù)就先把它入棧,然后 activeEffect 指向這個(gè) reactiveEffect 函數(shù),接著在 fn 執(zhí)行完畢后出棧,再把 activeEffect 指向 effectStack 最后一個(gè)元素,也就是外層 effect 函數(shù)對(duì)應(yīng)的 reactiveEffect。
這里我們還注意到一個(gè)細(xì)節(jié),在入棧前會(huì)執(zhí)行 cleanup 函數(shù)清空 reactiveEffect 函數(shù)對(duì)應(yīng)的依賴(lài) 。在執(zhí)行 track 函數(shù)的時(shí)候,除了收集當(dāng)前激活的 effect 作為依賴(lài),還通過(guò) activeEffect.deps.push(dep) 把 dep 作為 activeEffect 的依賴(lài),這樣在 cleanup 的時(shí)候我們就可以找到 effect 對(duì)應(yīng)的 dep 了,然后把 effect 從這些 dep 中刪除。cleanup 函數(shù)的代碼如下所示:
function?cleanup(effect)?{const?{?deps?}?=?effectif?(deps.length)?{for?(let?i?=?0;?i?<?deps.length;?i++)?{deps[i].delete(effect)}deps.length?=?0} }為什么需要 cleanup 呢?如果遇到這種場(chǎng)景:
<template><div v-if="state.showMsg">{{ state.msg }}</div><div v-else>{{ Math.random()}}</div><button @click="toggle">Toggle Msg</button><button @click="switchView">Switch View</button> </template> <script>import { reactive } from 'vue'export default {setup() {const state = reactive({msg: 'Hello World',showMsg: true})function toggle() {state.msg = state.msg === 'Hello World' ? 'Hello Vue' : 'Hello World'}function switchView() {state.showMsg = !state.showMsg}return {toggle,switchView,state}}} </script>結(jié)合代碼可以知道,這個(gè)組件的視圖會(huì)根據(jù) showMsg 變量的控制顯示 msg 或者一個(gè)隨機(jī)數(shù),當(dāng)我們點(diǎn)擊 Switch View 的按鈕時(shí),就會(huì)修改這個(gè)變量值。
假設(shè)沒(méi)有 cleanup,在第一次渲染模板的時(shí)候,activeEffect 是組件的副作用渲染函數(shù),因?yàn)槟0?render 的時(shí)候訪(fǎng)問(wèn)了 state.msg,所以會(huì)執(zhí)行依賴(lài)收集,把副作用渲染函數(shù)作為 state.msg 的依賴(lài),我們把它稱(chēng)作 render effect。然后我們點(diǎn)擊 Switch View 按鈕,視圖切換為顯示隨機(jī)數(shù),此時(shí)我們?cè)冱c(diǎn)擊 Toggle Msg 按鈕,由于修改了 state.msg 就會(huì)派發(fā)通知,找到了 render effect 并執(zhí)行,就又觸發(fā)了組件的重新渲染。
但這個(gè)行為實(shí)際上并不符合預(yù)期,因?yàn)楫?dāng)我們點(diǎn)擊 Switch View 按鈕,視圖切換為顯示隨機(jī)數(shù)的時(shí)候,也會(huì)觸發(fā)組件的重新渲染,但這個(gè)時(shí)候視圖并沒(méi)有渲染 state.msg,所以對(duì)它的改動(dòng)并不應(yīng)該影響組件的重新渲染。
因此在組件的 render effect 執(zhí)行之前,如果通過(guò) cleanup 清理依賴(lài),我們就可以刪除之前 state.msg 收集的 render effect 依賴(lài)。這樣當(dāng)我們修改 state.msg 時(shí),由于已經(jīng)沒(méi)有依賴(lài)了就不會(huì)觸發(fā)組件的重新渲染,符合預(yù)期。
響應(yīng)式實(shí)現(xiàn)的優(yōu)化
前面分析了響應(yīng)式實(shí)現(xiàn)原理,看上去一切都很 OK,那么這里面還有哪些可以值得優(yōu)化的點(diǎn)呢?
依賴(lài)收集的優(yōu)化
目前每次副作用函數(shù)執(zhí)行,都需要先執(zhí)行 cleanup 清除依賴(lài),然后在副作用函數(shù)執(zhí)行的過(guò)程中重新收集依賴(lài),這個(gè)過(guò)程牽涉到大量對(duì) Set 集合的添加和刪除操作。在許多場(chǎng)景下,依賴(lài)關(guān)系是很少改變的,因此這里存在一定的優(yōu)化空間。
為了減少集合的添加刪除操作,我們需要標(biāo)識(shí)每個(gè)依賴(lài)集合的狀態(tài),比如它是不是新收集的,還是已經(jīng)被收集過(guò)的。
所以這里需要給集合 dep 添加兩個(gè)屬性:
export?const?createDep?=?(effects)?=>?{const?dep?=?new?Set(effects)dep.w?=?0dep.n?=?0return?dep }其中 w 表示是否已經(jīng)被收集,n 表示是否新收集。
然后設(shè)計(jì)幾個(gè)全局變量,effectTrackDepth、trackOpBit、maxMarkerBits。
其中 effectTrackDepth 表示遞歸嵌套執(zhí)行 ?effect 函數(shù)的深度;trackOpBit 用于標(biāo)識(shí)依賴(lài)收集的狀態(tài);maxMarkerBits 表示最大標(biāo)記的位數(shù)。
接下來(lái)看它們的應(yīng)用:
function?effect(fn,?options)?{if?(fn.effect)?{fn?=?fn.effect.fn}//?創(chuàng)建?_effect?實(shí)例?const?_effect?=?new?ReactiveEffect(fn)if?(options)?{//?拷貝?options?中的屬性到?_effect?中extend(_effect,?options)if?(options.scope)//?effectScope?相關(guān)處理邏輯recordEffectScope(_effect,?options.scope)}if?(!options?||?!options.lazy)?{//?立即執(zhí)行_effect.run()}//?綁定?run?函數(shù),作為?effect?runnerconst?runner?=?_effect.run.bind(_effect)//?runner?中保留對(duì)?_effect?的引用runner.effect?=?_effectreturn?runner }class?ReactiveEffect?{constructor(fn,?scheduler?=?null,?scope)?{this.fn?=?fnthis.scheduler?=?schedulerthis.active?=?true//?effect?存儲(chǔ)相關(guān)的?deps?依賴(lài)this.deps?=?[]//?effectScope?相關(guān)處理邏輯recordEffectScope(this,?scope)}run()?{if?(!this.active)?{return?this.fn()}if?(!effectStack.includes(this))?{try?{//?壓棧effectStack.push((activeEffect?=?this))enableTracking()//?根據(jù)遞歸的深度記錄位數(shù)trackOpBit?=?1?<<?++effectTrackDepth//?超過(guò)?maxMarkerBits?則?trackOpBit?的計(jì)算會(huì)超過(guò)最大整形的位數(shù),降級(jí)為?cleanupEffectif?(effectTrackDepth?<=?maxMarkerBits)?{//?給依賴(lài)打標(biāo)記initDepMarkers(this)}else?{cleanupEffect(this)}return?this.fn()}finally?{if?(effectTrackDepth?<=?maxMarkerBits)?{//?完成依賴(lài)標(biāo)記finalizeDepMarkers(this)}//?恢復(fù)到上一級(jí)trackOpBit?=?1?<<?--effectTrackDepthresetTracking()//?出棧effectStack.pop()const?n?=?effectStack.length//?指向棧最后一個(gè)?effectactiveEffect?=?n?>?0???effectStack[n?-?1]?:?undefined}}}stop()?{if?(this.active)?{cleanupEffect(this)if?(this.onStop)?{this.onStop()}this.active?=?false}} }可以看到,effect 函數(shù)的實(shí)現(xiàn)做了一定的修改和調(diào)整,內(nèi)部使用 ReactiveEffect 類(lèi)創(chuàng)建了一個(gè) _effect 實(shí)例,并且函數(shù)返回的 runner 指向的是 ReactiveEffect 類(lèi)的 run 方法。
也就是執(zhí)行副作用函數(shù) effect 函數(shù)時(shí),實(shí)際上執(zhí)行的就是這個(gè) run 函數(shù)。
當(dāng) run 函數(shù)執(zhí)行的時(shí)候,我們注意到 cleanup 函數(shù)不再默認(rèn)執(zhí)行,在封裝的函數(shù) fn 執(zhí)行前,首先執(zhí)行 trackOpBit = 1 << ++effectTrackDepth 記錄 trackOpBit,然后對(duì)比遞歸深度是否超過(guò)了 maxMarkerBits,如果超過(guò)(通常情況下不會(huì))則仍然執(zhí)行老的 cleanup 邏輯,如果沒(méi)超過(guò)則執(zhí)行 initDepMarkers 給依賴(lài)打標(biāo)記,來(lái)看它的實(shí)現(xiàn):
const?initDepMarkers?=?({?deps?})?=>?{if?(deps.length)?{for?(let?i?=?0;?i?<?deps.length;?i++)?{deps[i].w?|=?trackOpBit?//?標(biāo)記依賴(lài)已經(jīng)被收集}} }initDepMarkers 函數(shù)實(shí)現(xiàn)很簡(jiǎn)單,遍歷 _effect 實(shí)例中的 deps 屬性,給每個(gè) dep 的 w 屬性標(biāo)記為 trackOpBit 的值。
接下來(lái)會(huì)執(zhí)行 fn 函數(shù),在就是副作用函數(shù)封裝的函數(shù),比如針對(duì)組件渲染,fn 就是組件渲染函數(shù)。
當(dāng) fn 函數(shù)執(zhí)行時(shí)候,會(huì)訪(fǎng)問(wèn)到響應(yīng)式數(shù)據(jù),就會(huì)觸發(fā)它們的 getter,進(jìn)而執(zhí)行 track 函數(shù)執(zhí)行依賴(lài)收集。相應(yīng)的,依賴(lài)收集的過(guò)程也做了一些調(diào)整:
function?track(target,?type,?key)?{if?(!isTracking())?{return}let?depsMap?=?targetMap.get(target)if?(!depsMap)?{//?每個(gè)?target?對(duì)應(yīng)一個(gè)?depsMaptargetMap.set(target,?(depsMap?=?new?Map()))}let?dep?=?depsMap.get(key)if?(!dep)?{//?每個(gè)?key?對(duì)應(yīng)一個(gè)?dep?集合depsMap.set(key,?(dep?=?createDep()))}const?eventInfo?=?(process.env.NODE_ENV?!==?'production')??{?effect:?activeEffect,?target,?type,?key?}:?undefinedtrackEffects(dep,?eventInfo) }function?trackEffects(dep,?debuggerEventExtraInfo)?{let?shouldTrack?=?falseif?(effectTrackDepth?<=?maxMarkerBits)?{if?(!newTracked(dep))?{//?標(biāo)記為新依賴(lài)dep.n?|=?trackOpBit?//?如果依賴(lài)已經(jīng)被收集,則不需要再次收集shouldTrack?=?!wasTracked(dep)}}else?{//?cleanup?模式shouldTrack?=?!dep.has(activeEffect)}if?(shouldTrack)?{//?收集當(dāng)前激活的?effect?作為依賴(lài)dep.add(activeEffect)//?當(dāng)前激活的?effect?收集?dep?集合作為依賴(lài)activeEffect.deps.push(dep)if?((process.env.NODE_ENV?!==?'production')?&&?activeEffect.onTrack)?{activeEffect.onTrack(Object.assign({effect:?activeEffect},?debuggerEventExtraInfo))}} }我們發(fā)現(xiàn),當(dāng)創(chuàng)建 dep 的時(shí)候,是通過(guò)執(zhí)行 createDep 方法完成的,此外,在 dep 把前激活的 effect 作為依賴(lài)收集前,會(huì)判斷這個(gè) dep 是否已經(jīng)被收集,如果已經(jīng)被收集,則不需要再次收集了。此外,這里還會(huì)判斷這 dep 是不是新的依賴(lài),如果不是,則標(biāo)記為新的。
接下來(lái),我們?cè)賮?lái)看 fn 執(zhí)行完后的邏輯:
finally?{if?(effectTrackDepth?<=?maxMarkerBits)?{//?完成依賴(lài)標(biāo)記finalizeDepMarkers(this)}//?恢復(fù)到上一級(jí)trackOpBit?=?1?<<?--effectTrackDepthresetTracking()//?出棧effectStack.pop()const?n?=?effectStack.length//?指向棧最后一個(gè)?effectactiveEffect?=?n?>?0???effectStack[n?-?1]?:?undefined }在滿(mǎn)足依賴(lài)標(biāo)記的條件下,需要執(zhí)行 finalizeDepMarkers 完成依賴(lài)標(biāo)記,來(lái)看它的實(shí)現(xiàn):
const?finalizeDepMarkers?=?(effect)?=>?{const?{?deps?}?=?effectif?(deps.length)?{let?ptr?=?0for?(let?i?=?0;?i?<?deps.length;?i++)?{const?dep?=?deps[i]//?曾經(jīng)被收集過(guò)但不是新的依賴(lài),需要?jiǎng)h除if?(wasTracked(dep)?&&?!newTracked(dep))?{dep.delete(effect)}else?{deps[ptr++]?=?dep}//?清空狀態(tài)dep.w?&=?~trackOpBitdep.n?&=?~trackOpBit}deps.length?=?ptr} }finalizeDepMarkers 主要做的事情就是找到那些曾經(jīng)被收集過(guò)但是新的一輪依賴(lài)收集沒(méi)有被收集的依賴(lài),從 deps 中移除。這其實(shí)就是解決前面提到的需要 cleanup 場(chǎng)景的問(wèn)題:在新的組件渲染過(guò)程中沒(méi)有訪(fǎng)問(wèn)到的響應(yīng)式對(duì)象,那么它的變化不應(yīng)該觸發(fā)組件的重新渲染。
以上就實(shí)現(xiàn)了依賴(lài)收集部分的優(yōu)化,可以看到相比于之前每次執(zhí)行 effect 函數(shù)都需要先清空依賴(lài),再添加依賴(lài)的過(guò)程,現(xiàn)在的實(shí)現(xiàn)會(huì)在每次執(zhí)行 effect 包裹的函數(shù)前標(biāo)記依賴(lài)的狀態(tài),過(guò)程中對(duì)于已經(jīng)收集的依賴(lài)不會(huì)重復(fù)收集,執(zhí)行完 effect 函數(shù)還會(huì)移除掉已被收集但是新的一輪依賴(lài)收集中沒(méi)有被收集的依賴(lài)。
優(yōu)化后對(duì)于 dep 依賴(lài)集合的操作減少了,自然也就優(yōu)化了性能。
響應(yīng)式 API 的優(yōu)化
響應(yīng)式 API 的優(yōu)化主要體現(xiàn)在對(duì) ref、computed 等 API 的優(yōu)化。
以 ref API 為例,來(lái)看看它優(yōu)化前的實(shí)現(xiàn):
function?ref(value)?{return?createRef(value) }const?convert?=?(val)?=>?isObject(val)???reactive(val)?:?valfunction?createRef(rawValue,?shallow?=?false)?{if?(isRef(rawValue))?{//?如果傳入的就是一個(gè) ref,那么返回自身即可,處理嵌套 ref 的情況。return?rawValue}return?new?RefImpl(rawValue,?shallow) }class?RefImpl?{constructor(_rawValue,?_shallow?=?false)?{this._rawValue?=?_rawValuethis._shallow?=?_shallowthis.__v_isRef?=?true//?非?shallow?的情況,如果它的值是對(duì)象或者數(shù)組,則遞歸響應(yīng)式this._value?=?_shallow???_rawValue?:?convert(_rawValue)}get?value()?{//?給?value?屬性添加?getter,并做依賴(lài)收集track(toRaw(this),?'get'?/*?GET?*/,?'value')return?this._value}set?value(newVal)?{//?給?value?屬性添加?setterif?(hasChanged(toRaw(newVal),?this._rawValue))?{this._rawValue?=?newValthis._value?=?this._shallow???newVal?:?convert(newVal)//?派發(fā)通知trigger(toRaw(this),?'set'?/*?SET?*/,?'value',?newVal)}} }ref 函數(shù)返回了 createRef 函數(shù)執(zhí)行的返回值,而在 createRef 內(nèi)部,首先處理了嵌套 ref 的情況,如果傳入的 rawValue 也是個(gè) ref,那么直接返回 rawValue;接著返回 RefImpl 對(duì)象的實(shí)例。
而 RefImpl 內(nèi)部的實(shí)現(xiàn),主要是劫持它的實(shí)例 value 屬性的 getter 和 setter。
當(dāng)訪(fǎng)問(wèn)一個(gè) ref 對(duì)象的 value 屬性,會(huì)觸發(fā) getter 執(zhí)行 track 函數(shù)做依賴(lài)收集然后返回它的值;當(dāng)修改一個(gè) ref 對(duì)象的 value 值,則會(huì)觸發(fā) setter 設(shè)置新值并且執(zhí)行 trigger 函數(shù)派發(fā)通知,如果新值 newVal 是對(duì)象或者數(shù)組類(lèi)型,那么把它轉(zhuǎn)換成一個(gè) reactive 對(duì)象。
接下來(lái),我們?cè)賮?lái)看 Vue.js 3.2 對(duì)于這部分的實(shí)現(xiàn)相關(guān)的改動(dòng):
class?RefImpl?{constructor(value,?_shallow?=?false)?{this._shallow?=?_shallowthis.dep?=?undefinedthis.__v_isRef?=?truethis._rawValue?=?_shallow???value?:?toRaw(value)this._value?=?_shallow???value?:?convert(value)}get?value()?{trackRefValue(this)return?this._value}set?value(newVal)?{newVal?=?this._shallow???newVal?:?toRaw(newVal)if?(hasChanged(newVal,?this._rawValue))?{this._rawValue?=?newValthis._value?=?this._shallow???newVal?:?convert(newVal)triggerRefValue(this,?newVal)}} }主要改動(dòng)部分就是對(duì) ref 對(duì)象的 value 屬性執(zhí)行依賴(lài)收集和派發(fā)通知的邏輯。
在 Vue.js 3.2 版本的 ref 的實(shí)現(xiàn)中,關(guān)于依賴(lài)收集部分,由原先的 track 函數(shù)改成了 trackRefValue,來(lái)看它的實(shí)現(xiàn):
function?trackRefValue(ref)?{if?(isTracking())?{ref?=?toRaw(ref)if?(!ref.dep)?{ref.dep?=?createDep()}if?((process.env.NODE_ENV?!==?'production'))?{trackEffects(ref.dep,?{target:?ref,type:?"get"?/*?GET?*/,key:?'value'})}else?{trackEffects(ref.dep)}} }可以看到這里直接把 ref 的相關(guān)依賴(lài)保存到 dep 屬性中,而在 track 函數(shù)的實(shí)現(xiàn)中,會(huì)把依賴(lài)保留到全局的 targetMap 中:
let?depsMap?=?targetMap.get(target) if?(!depsMap)?{//?每個(gè)?target?對(duì)應(yīng)一個(gè)?depsMaptargetMap.set(target,?(depsMap?=?new?Map())) } let?dep?=?depsMap.get(key) if?(!dep)?{//?每個(gè)?key?對(duì)應(yīng)一個(gè)?dep?集合depsMap.set(key,?(dep?=?createDep())) }顯然,track 函數(shù)內(nèi)部可能需要做多次判斷和設(shè)置邏輯,而把依賴(lài)保存到 ref 對(duì)象的 dep 屬性中則省去了這一系列的判斷和設(shè)置,從而優(yōu)化性能。
相應(yīng)的,ref 的實(shí)現(xiàn)關(guān)于派發(fā)通知部分,由原先的 trigger 函數(shù)改成了 triggerRefValue,來(lái)看它的實(shí)現(xiàn):
function?triggerRefValue(ref,?newVal)?{ref?=?toRaw(ref)if?(ref.dep)?{if?((process.env.NODE_ENV?!==?'production'))?{triggerEffects(ref.dep,?{target:?ref,type:?"set"?/*?SET?*/,key:?'value',newValue:?newVal})}else?{triggerEffects(ref.dep)}} }function?triggerEffects(dep,?debuggerEventExtraInfo)?{for?(const?effect?of?isArray(dep)???dep?:?[...dep])?{if?(effect?!==?activeEffect?||?effect.allowRecurse)?{if?((process.env.NODE_ENV?!==?'production')?&&?effect.onTrigger)?{effect.onTrigger(extend({?effect?},?debuggerEventExtraInfo))}if?(effect.scheduler)?{effect.scheduler()}else?{effect.run()}}} }由于直接從 ref 屬性中就拿到了它所有的依賴(lài)且遍歷執(zhí)行,不需要執(zhí)行 trigger 函數(shù)一些額外的查找邏輯,因此在性能上也得到了提升。
trackOpBit 的設(shè)計(jì)
細(xì)心的你可能會(huì)發(fā)現(xiàn),標(biāo)記依賴(lài)的 trackOpBit,在每次計(jì)算時(shí)采用了左移的運(yùn)算符 trackOpBit = 1 << ++effectTrackDepth;并且在賦值的時(shí)候,使用了或運(yùn)算:
deps[i].w?|=?trackOpBit dep.n?|=?trackOpBit那么為什么這么設(shè)計(jì)呢?因?yàn)?effect 的執(zhí)行可能會(huì)有遞歸的情況,通過(guò)這種方式就可以記錄每個(gè)層級(jí)的依賴(lài)標(biāo)記情況。
在判斷某個(gè) dep 是否已經(jīng)被依賴(lài)收集的時(shí)候,使用了 wasTracked 函數(shù):
const?wasTracked?=?(dep)?=>?(dep.w?&?trackOpBit)?>?0通過(guò)與運(yùn)算的結(jié)果是否大于 0 來(lái)判斷,這就要求依賴(lài)被收集時(shí)嵌套的層級(jí)要匹配。舉個(gè)例子,假設(shè)此時(shí) dep.w 的值是 2,說(shuō)明它是在第一層執(zhí)行 effect 函數(shù)時(shí)創(chuàng)建的,但是這時(shí)候已經(jīng)執(zhí)行了嵌套在第二層的 effect 函數(shù),trackOpBit 左移兩位變成了 4,2 & 4 的值是 0,那么 wasTracked 函數(shù)返回值為 false,說(shuō)明需要收集這個(gè)依賴(lài)。顯然,這個(gè)需求是合理的。
可以看到,如果沒(méi)有 trackOpBit 位運(yùn)算的設(shè)計(jì),你就很難去處理不同嵌套層級(jí)的依賴(lài)標(biāo)記,這個(gè)設(shè)計(jì)也體現(xiàn)了 basvanmeurs 大佬非常扎實(shí)的計(jì)算機(jī)基礎(chǔ)功力。
總結(jié)
一般在 Vue.js 的應(yīng)用中,對(duì)響應(yīng)式數(shù)據(jù)的訪(fǎng)問(wèn)和修改都是非常頻繁的操作,因此對(duì)這個(gè)過(guò)程的性能優(yōu)化,將極大提升整個(gè)應(yīng)用的性能。
大部分人去看 Vue.js 響應(yīng)式的實(shí)現(xiàn),可能目標(biāo)最多就是搞明白其中的實(shí)現(xiàn)原理,而很少去關(guān)注其中實(shí)現(xiàn)是否是最優(yōu)的。而 basvanmeurs 大佬能對(duì)提出這一系列的優(yōu)化的實(shí)現(xiàn),并且手寫(xiě)了一個(gè) benchmark 工具來(lái)驗(yàn)證自己的優(yōu)化,非常值得我們學(xué)習(xí)。
希望你看完這篇文章,除了點(diǎn)贊在看轉(zhuǎn)發(fā)三連之外,也可以去看看原貼,看看他們的討論,相信你會(huì)收獲更多。
前端的性能優(yōu)化永遠(yuǎn)是一個(gè)值得深挖的方向,希望在日后的開(kāi)發(fā)中,不論是寫(xiě)框架還是業(yè)務(wù),你都能夠經(jīng)常去思考其中可能存在的優(yōu)化的點(diǎn)。
參考資料
[1]?Vue.js 3.2 升級(jí)介紹: https://blog.vuejs.org/posts/vue-3.2.html
[2] basvanmeurs GitHub 地址:https://github.com/basvanmeurs
[3] 相關(guān) PR 討論地址:https://github.com/vuejs/vue-next/pull/2345
[4] Thoughts on ES6 Proxies Performance: https://thecodebarbarian.com/thoughts-on-es6-proxies-performance
[5] Proxy-vs-DefineProperty repo: https://github.com/ustbhuangyi/Proxy-vs-DefineProperty
[6 ]benchmark 工具: https://github.com/basvanmeurs/vue-next-benchmarks
最近組建了一個(gè)湖南人的前端交流群,如果你是湖南人可以加我微信?ruochuan12?私信 湖南?拉你進(jìn)群。
推薦閱讀
我在阿里招前端,該怎么幫你(可進(jìn)面試群)
我讀源碼的經(jīng)歷
面對(duì) this 指向丟失,尤雨溪在 Vuex 源碼中是怎么處理的
老姚淺談:怎么學(xué)JavaScript?
·················?若川簡(jiǎn)介?·················
你好,我是若川,畢業(yè)于江西高校。現(xiàn)在是一名前端開(kāi)發(fā)“工程師”。寫(xiě)有《學(xué)習(xí)源碼整體架構(gòu)系列》多篇,在知乎、掘金收獲超百萬(wàn)閱讀。
從2014年起,每年都會(huì)寫(xiě)一篇年度總結(jié),已經(jīng)寫(xiě)了7篇,點(diǎn)擊查看年度總結(jié)。
同時(shí),活躍在知乎@若川,掘金@若川。致力于分享前端開(kāi)發(fā)經(jīng)驗(yàn),愿景:幫助5年內(nèi)前端人走向前列。
識(shí)別上方二維碼加我微信、拉你進(jìn)源碼共讀群
今日話(huà)題
略。歡迎分享、收藏、點(diǎn)贊、在看我的公眾號(hào)文章~
總結(jié)
以上是生活随笔為你收集整理的细说 Vue.js 3.2 关于响应式部分的优化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【Direct3D - 6】Direct
- 下一篇: 学习尤雨溪写的 Vue3 源码中的简单工