Vue.js3.2響應(yīng)式部分的優(yōu)化升級(jí)詳解
背景
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ì)于用戶的使用層面來說其實(shí)變化并不大。其中一個(gè)吸引我的點(diǎn)是提升了響應(yīng)式的性能:
More efficient ref implementation (~260% faster read / ~50% faster write)
~40% faster dependency tracking
~17% less memory usage
翻譯過來就是 ref API 的讀效率提升約為 260%,寫效率提升約為 50% ,依賴收集的效率提升約為 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 開發(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)檫@么核心的代碼能來自社區(qū)的貢獻(xiàn),這就意味著 Vue 3 受到越來越多的人關(guān)注;一些能力強(qiáng)的開發(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í)候,官方宣稱在響應(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 還是選擇了它來實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式呢?因?yàn)?Proxy 本質(zhì)上是對(duì)某個(gè)對(duì)象的劫持,這樣它不僅僅可以監(jiān)聽對(duì)象某個(gè)屬性值的變化,還可以監(jiān)聽對(duì)象屬性的新增和刪除;而 Object.defineProperty 是給對(duì)象的某個(gè)已存在的屬性添加對(duì)應(yīng)的 getter 和 setter,所以它只能監(jiān)聽這個(gè)屬性值的變化,而不能去監(jiān)聽對(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ì)象屬性被訪問的時(shí)候才會(huì)判斷子屬性的類型來決定要不要遞歸執(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ì)的飛躍,接下來我們就來上點(diǎn)硬菜,從源碼層面分析具體做了哪些優(yōu)化,以及這些優(yōu)化背后帶來的技術(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ì)上是通過 Proxy API 劫持了數(shù)據(jù)對(duì)象的讀寫,當(dāng)我們?cè)L問數(shù)據(jù)時(shí),會(huì)觸發(fā) getter 執(zhí)行依賴收集;修改數(shù)據(jù)時(shí),會(huì)觸發(fā) setter 派發(fā)通知。
接下來,我們簡(jiǎn)單分析一下依賴收集和派發(fā)通知的實(shí)現(xiàn)(Vue.js 3.2 之前的版本)。
依賴收集
首先來看依賴收集的過程,核心就是在訪問響應(yīng)式數(shù)據(jù)的時(shí)候,觸發(fā) getter 函數(shù),進(jìn)而執(zhí)行 track 函數(shù)收集依賴:
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è) depsMap
targetMap.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 作為依賴
dep.add(activeEffect)
// 當(dāng)前激活的 effect 收集 dep 集合作為依賴
activeEffect.deps.push(dep)
}
}
分析這個(gè)函數(shù)的實(shí)現(xiàn)前,我們先想一下要收集的依賴是什么,我們的目的是實(shí)現(xiàn)響應(yīng)式,就是當(dāng)數(shù)據(jù)變化的時(shí)候可以自動(dòng)做一些事情,比如執(zhí)行某些函數(shù),所以我們收集的依賴就是數(shù)據(jù)變化后執(zhí)行的副作用函數(shù)。
track 函數(shù)擁有三個(gè)參數(shù),其中 target 表示原始數(shù)據(jù);type 表示這次依賴收集的類型;key 表示訪問的屬性。
track 函數(shù)外部創(chuàng)建了全局的 targetMap 作為原始數(shù)據(jù)對(duì)象的 Map,它的鍵是 target,值是 depsMap,作為依賴的 Map;這個(gè) depsMap 的鍵是 target 的 key,值是 dep 集合,dep 集合中存儲(chǔ)的是依賴的副作用函數(shù)。為了方便理解,可以通過下圖表示它們之間的關(guān)系:

因此每次執(zhí)行 track 函數(shù),就是把當(dāng)前激活的副作用函數(shù) activeEffect 作為依賴,然后收集到 target 相關(guān)的 depsMap 對(duì)應(yīng) key 下的依賴集合 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) {
// 通過 targetMap 拿到 target 對(duì)應(yīng)的依賴集合
const depsMap = targetMap.get(target)
if (!depsMap) {
// 沒有依賴,直接返回
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)的 effects
if (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í)行 effects
effects.forEach(run)
}
trigger 函數(shù)擁有三個(gè)參數(shù),其中 target 表示目標(biāo)原始對(duì)象;type 表示更新的類型;key 表示要修改的屬性。
trigger 函數(shù) 主要做了四件事情:
- 從
targetMap中拿到target對(duì)應(yīng)的依賴集合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í)行一遍。
在描述依賴收集和派發(fā)通知的過程中,我們都提到了一個(gè)詞:副作用函數(shù),依賴收集過程中我們把 activeEffect(當(dāng)前激活副作用函數(shù))作為依賴收集,它又是什么?接下來我們來看一下副作用函數(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 中訪問了 counter.num,我們希望在執(zhí)行 count 函數(shù)修改 counter.num 值的時(shí)候,能自動(dòng)執(zhí)行 logCount 函數(shù)。
按我們之前對(duì)依賴收集過程的分析,如果logCount 是 activeEffect 的話,那么就可以實(shí)現(xiàn)需求,但顯然是做不到的,因?yàn)榇a在執(zhí)行到 console.log(counter.num) 這一行的時(shí)候,它對(duì)自己在 logCount 函數(shù)中的運(yùn)行是一無所知的。
那么該怎么辦呢?其實(shí)只要我們運(yùn)行 logCount 函數(shù)前,把 logCount 賦值給 activeEffect 就好了:
activeEffect = logCount logCount()
順著這個(gè)思路,我們可以利用高階函數(shù)的思想,對(duì) logCount 做一層封裝:
function wrapper(fn) {
const wrapped = function(...args) {
activeEffect = fn
fn(...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 就是采用類似的做法,在它內(nèi)部就有一個(gè) effect 副作用函數(shù),我們來看一下它的實(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 引用的依賴
cleanup(effect)
try {
// 開啟全局 shouldTrack,允許依賴收集
enableTracking()
// 壓棧
effectStack.push(effect)
activeEffect = effect
// 執(zhí)行原始函數(shù)
return fn()
}
finally {
// 出棧
effectStack.pop()
// 恢復(fù) shouldTrack 開啟之前的狀態(tài)
resetTracking()
// 指向棧最后一個(gè) effect
activeEffect = 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)的依賴,雙向指針,依賴包含對(duì) effect 的引用,effect 也包含對(duì)依賴的引用
effect.deps = []
// effect 的相關(guān)配置
effect.options = options
return effect
}
結(jié)合上述代碼來看,effect 內(nèi)部通過執(zhí)行 createReactiveEffect 函數(shù)去創(chuàng)建一個(gè)新的 effect 函數(shù),為了和外部的 effect 函數(shù)區(qū)分,我們把它稱作 reactiveEffect 函數(shù),并且還給它添加了一些額外屬性(我在注釋中都有標(biāo)明)。另外,effect 函數(shù)還支持傳入一個(gè)配置參數(shù)以支持更多的 feature,這里就不展開了。
reactiveEffect 函數(shù)就是響應(yīng)式的副作用函數(shù),當(dāng)執(zhí)行 trigger 過程派發(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,如果沒有就把 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ù)訪問 counter.num 的時(shí)候,依賴收集對(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)的依賴 。在執(zhí)行 track 函數(shù)的時(shí)候,除了收集當(dāng)前激活的 effect 作為依賴,還通過 activeEffect.deps.push(dep) 把 dep 作為 activeEffect 的依賴,這樣在 cleanup 的時(shí)候我們就可以找到 effect 對(duì)應(yīng)的 dep 了,然后把 effect 從這些 dep 中刪除。cleanup 函數(shù)的代碼如下所示:
function cleanup(effect) {
const { deps } = effect
if (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è)沒有 cleanup,在第一次渲染模板的時(shí)候,activeEffect 是組件的副作用渲染函數(shù),因?yàn)槟0?render 的時(shí)候訪問了 state.msg,所以會(huì)執(zhí)行依賴收集,把副作用渲染函數(shù)作為 state.msg 的依賴,我們把它稱作 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í)候視圖并沒有渲染 state.msg,所以對(duì)它的改動(dòng)并不應(yīng)該影響組件的重新渲染。
因此在組件的 render effect 執(zhí)行之前,如果通過 cleanup 清理依賴,我們就可以刪除之前 state.msg 收集的 render effect 依賴。這樣當(dāng)我們修改 state.msg 時(shí),由于已經(jīng)沒有依賴了就不會(huì)觸發(fā)組件的重新渲染,符合預(yù)期。
響應(yīng)式實(shí)現(xiàn)的優(yōu)化
前面分析了響應(yīng)式實(shí)現(xiàn)原理,看上去一切都很 OK,那么這里面還有哪些可以值得優(yōu)化的點(diǎn)呢?
依賴收集的優(yōu)化
目前每次副作用函數(shù)執(zhí)行,都需要先執(zhí)行 cleanup 清除依賴,然后在副作用函數(shù)執(zhí)行的過程中重新收集依賴,這個(gè)過程牽涉到大量對(duì) Set 集合的添加和刪除操作。在許多場(chǎng)景下,依賴關(guān)系是很少改變的,因此這里存在一定的優(yōu)化空間。
為了減少集合的添加刪除操作,我們需要標(biāo)識(shí)每個(gè)依賴集合的狀態(tài),比如它是不是新收集的,還是已經(jīng)被收集過的。
所以這里需要給集合 dep 添加兩個(gè)屬性:
export const createDep = (effects) => {
const dep = new Set(effects)
dep.w = 0
dep.n = 0
return dep
}
其中 w 表示是否已經(jīng)被收集,n 表示是否新收集。
然后設(shè)計(jì)幾個(gè)全局變量,effectTrackDepth、trackOpBit、maxMarkerBits。
其中 effectTrackDepth 表示遞歸嵌套執(zhí)行 effect 函數(shù)的深度;trackOpBit 用于標(biāo)識(shí)依賴收集的狀態(tài);maxMarkerBits 表示最大標(biāo)記的位數(shù)。
接下來看它們的應(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 runner
const runner = _effect.run.bind(_effect)
// runner 中保留對(duì) _effect 的引用
runner.effect = _effect
return runner
}
class ReactiveEffect {
constructor(fn, scheduler = null, scope) {
this.fn = fn
this.scheduler = scheduler
this.active = true
// effect 存儲(chǔ)相關(guān)的 deps 依賴
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
// 超過 maxMarkerBits 則 trackOpBit 的計(jì)算會(huì)超過最大整形的位數(shù),降級(jí)為 cleanupEffect
if (effectTrackDepth <= maxMarkerBits) {
// 給依賴打標(biāo)記
initDepMarkers(this)
}
else {
cleanupEffect(this)
}
return this.fn()
}
finally {
if (effectTrackDepth <= maxMarkerBits) {
// 完成依賴標(biāo)記
finalizeDepMarkers(this)
}
// 恢復(fù)到上一級(jí)
trackOpBit = 1 << --effectTrackDepth
resetTracking()
// 出棧
effectStack.pop()
const n = effectStack.length
// 指向棧最后一個(gè) effect
activeEffect = 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 類創(chuàng)建了一個(gè) _effect 實(shí)例,并且函數(shù)返回的 runner 指向的是 ReactiveEffect 類的 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ì)比遞歸深度是否超過了 maxMarkerBits,如果超過(通常情況下不會(huì))則仍然執(zhí)行老的 cleanup 邏輯,如果沒超過則執(zhí)行 initDepMarkers 給依賴打標(biāo)記,來看它的實(shí)現(xiàn):
const initDepMarkers = ({ deps }) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit // 標(biāo)記依賴已經(jīng)被收集
}
}
}
initDepMarkers 函數(shù)實(shí)現(xiàn)很簡(jiǎn)單,遍歷 _effect 實(shí)例中的 deps 屬性,給每個(gè) dep 的 w 屬性標(biāo)記為 trackOpBit 的值。
接下來會(huì)執(zhí)行 fn 函數(shù),在就是副作用函數(shù)封裝的函數(shù),比如針對(duì)組件渲染,fn 就是組件渲染函數(shù)。
當(dāng) fn 函數(shù)執(zhí)行時(shí)候,會(huì)訪問到響應(yīng)式數(shù)據(jù),就會(huì)觸發(fā)它們的 getter,進(jìn)而執(zhí)行 track 函數(shù)執(zhí)行依賴收集。相應(yīng)的,依賴收集的過程也做了一些調(diào)整:
function track(target, type, key) {
if (!isTracking()) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
// 每個(gè) target 對(duì)應(yīng)一個(gè) depsMap
targetMap.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 }
: undefined
trackEffects(dep, eventInfo)
}
function trackEffects(dep, debuggerEventExtraInfo) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
// 標(biāo)記為新依賴
dep.n |= trackOpBit
// 如果依賴已經(jīng)被收集,則不需要再次收集
shouldTrack = !wasTracked(dep)
}
}
else {
// cleanup 模式
shouldTrack = !dep.has(activeEffect)
}
if (shouldTrack) {
// 收集當(dāng)前激活的 effect 作為依賴
dep.add(activeEffect)
// 當(dāng)前激活的 effect 收集 dep 集合作為依賴
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í)候,是通過執(zhí)行 createDep 方法完成的,此外,在 dep 把前激活的 effect 作為依賴收集前,會(huì)判斷這個(gè) dep 是否已經(jīng)被收集,如果已經(jīng)被收集,則不需要再次收集了。此外,這里還會(huì)判斷這 dep 是不是新的依賴,如果不是,則標(biāo)記為新的。
接下來,我們?cè)賮砜?fn 執(zhí)行完后的邏輯:
finally {
if (effectTrackDepth <= maxMarkerBits) {
// 完成依賴標(biāo)記
finalizeDepMarkers(this)
}
// 恢復(fù)到上一級(jí)
trackOpBit = 1 << --effectTrackDepth
resetTracking()
// 出棧
effectStack.pop()
const n = effectStack.length
// 指向棧最后一個(gè) effect
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
在滿足依賴標(biāo)記的條件下,需要執(zhí)行 finalizeDepMarkers 完成依賴標(biāo)記,來看它的實(shí)現(xiàn):
const finalizeDepMarkers = (effect) => {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
// 曾經(jīng)被收集過但不是新的依賴,需要?jiǎng)h除
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect)
}
else {
deps[ptr++] = dep
}
// 清空狀態(tài)
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
deps.length = ptr
}
}
finalizeDepMarkers 主要做的事情就是找到那些曾經(jīng)被收集過但是新的一輪依賴收集沒有被收集的依賴,從 deps 中移除。這其實(shí)就是解決前面舉的需要 cleanup 的場(chǎng)景:在新的組件渲染過程中沒有訪問到的響應(yīng)式對(duì)象,那么它的變化不應(yīng)該觸發(fā)組件的重新渲染。
以上就實(shí)現(xiàn)了依賴收集部分的優(yōu)化,可以看到相比于之前每次執(zhí)行 effect 函數(shù)都需要先清空依賴,再添加依賴的過程,現(xiàn)在的實(shí)現(xiàn)會(huì)在每次執(zhí)行 effect 包裹的函數(shù)前標(biāo)記依賴的狀態(tài),過程中對(duì)于已經(jīng)收集的依賴不會(huì)重復(fù)收集,執(zhí)行完 effect 函數(shù)還會(huì)移除掉已被收集但是新的一輪依賴收集中沒有被收集的依賴。
優(yōu)化后對(duì)于 dep 依賴集合的操作就減少了,自然也就優(yōu)化了性能。
響應(yīng)式 API 的優(yōu)化
響應(yīng)式 API 的優(yōu)化主要體現(xiàn)在對(duì) ref、computed 等 API 的優(yōu)化。
以 ref API 為例,來看看它優(yōu)化前的實(shí)現(xiàn):
function ref(value) {
return createRef(value)
}
const convert = (val) => isObject(val) ? reactive(val) : val
function 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 = _rawValue
this._shallow = _shallow
this.__v_isRef = true
// 非 shallow 的情況,如果它的值是對(duì)象或者數(shù)組,則遞歸響應(yīng)式
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
// 給 value 屬性添加 getter,并做依賴收集
track(toRaw(this), 'get' /* GET */, 'value')
return this._value
}
set value(newVal) {
// 給 value 屬性添加 setter
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._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)訪問一個(gè) ref 對(duì)象的 value 屬性,會(huì)觸發(fā) getter 執(zhí)行 track 函數(shù)做依賴收集然后返回它的值;當(dāng)修改一個(gè) ref 對(duì)象的 value 值,則會(huì)觸發(fā) setter 設(shè)置新值并且執(zhí)行 trigger 函數(shù)派發(fā)通知,如果新值 newVal 是對(duì)象或者數(shù)組類型,那么把它轉(zhuǎn)換成一個(gè) reactive 對(duì)象。
接下來,我們?cè)賮砜?Vue.js 3.2 對(duì)于這部分的實(shí)現(xiàn)相關(guān)的改動(dòng):
class RefImpl {
constructor(value, _shallow = false) {
this._shallow = _shallow
this.dep = undefined
this.__v_isRef = true
this._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 = newVal
this._value = this._shallow ? newVal : convert(newVal)
triggerRefValue(this, newVal)
}
}
}
主要改動(dòng)部分就是對(duì) ref 對(duì)象的 value 屬性執(zhí)行依賴收集和派發(fā)通知的邏輯。
在 Vue.js 3.2 版本的 ref 的實(shí)現(xiàn)中,關(guān)于依賴收集部分,由原先的 track 函數(shù)改成了 trackRefValue,來看它的實(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)依賴保存到 dep 屬性中,而在 track 函數(shù)的實(shí)現(xiàn)中,會(huì)把依賴保留到全局的 targetMap 中:
let depsMap = targetMap.get(target)
if (!depsMap) {
// 每個(gè) target 對(duì)應(yīng)一個(gè) depsMap
targetMap.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è)置邏輯,而把依賴保存到 ref 對(duì)象的 dep 屬性中則省去了這一系列的判斷和設(shè)置,從而優(yōu)化性能。
相應(yīng)的,ref 的實(shí)現(xiàn)關(guān)于派發(fā)通知部分,由原先的 trigger 函數(shù)改成了 triggerRefValue,來看它的實(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 屬性中就拿到了它所有的依賴且遍歷執(zhí)行,不需要執(zhí)行 trigger 函數(shù)一些額外的邏輯,因此在性能上也得到了提升。
trackOpBit 的設(shè)計(jì)
細(xì)心的你可能會(huì)發(fā)現(xiàn),標(biāo)記依賴的 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ì)有遞歸的情況,通過這種方式就可以記錄每個(gè)層級(jí)的依賴標(biāo)記情況。
在判斷某個(gè) dep 是否已經(jīng)被依賴收集的時(shí)候,使用了 wasTracked 函數(shù):
const wasTracked = (dep) => (dep.w & trackOpBit) > 0
通過與運(yùn)算的結(jié)果是否大于 0 來判斷,這就要求依賴被收集時(shí)嵌套的層級(jí)要匹配。舉個(gè)例子,假設(shè)此時(shí) dep.w 的值是 2,說明它是在第一層執(zhí)行 effect 函數(shù)時(shí)創(chuàng)建的,但是這時(shí)候已經(jīng)執(zhí)行了嵌套在第二層的 effect 函數(shù),trackOpBit 左移兩位變成了 4,2 & 4 的值是 0,那么 wasTracked 函數(shù)返回值為 false,說明需要收集這個(gè)依賴。顯然,這個(gè)需求是合理的。
可以看到,如果沒有 trackOpBit 位運(yùn)算的設(shè)計(jì),你就很難去處理不同嵌套層級(jí)的依賴標(biāo)記,這個(gè)設(shè)計(jì)也體現(xiàn)了 basvanmeurs 大佬非常扎實(shí)的計(jì)算機(jī)基礎(chǔ)功力。
總結(jié)
一般在 Vue.js 的應(yīng)用中,對(duì)響應(yīng)式數(shù)據(jù)的訪問和修改都是非常頻繁的操作,因此對(duì)這個(gè)過程的性能優(yōu)化,將極大提升整個(gè)應(yīng)用的性能。
大部分人去看 Vue.js 響應(yīng)式的實(shí)現(xiàn),可能目的最多就是搞明白其中的實(shí)現(xiàn)原理,而很少去關(guān)注其中實(shí)現(xiàn)是否是最優(yōu)的。而 basvanmeurs 大佬能對(duì)提出這一系列的優(yōu)化的實(shí)現(xiàn),并且手寫了一個(gè) benchmark 工具 來驗(yàn)證自己的優(yōu)化,非常值得我們學(xué)習(xí)。
原貼,看看他們的討論,相信你會(huì)收獲更多。
前端的性能優(yōu)化永遠(yuǎn)是一個(gè)值得深挖的方向,希望在日后的開發(fā)中,不論是寫框架還是業(yè)務(wù),你都能夠經(jīng)常去思考其中可能存在的優(yōu)化的點(diǎn)。
以上就是Vue.js3.2響應(yīng)式部分的優(yōu)化升級(jí)詳解的詳細(xì)內(nèi)容,更多關(guān)于Vue.js3.2響應(yīng)式優(yōu)化升級(jí)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue+Echarts實(shí)現(xiàn)簡(jiǎn)單折線圖
這篇文章主要為大家詳細(xì)介紹了Vue+Echarts實(shí)現(xiàn)簡(jiǎn)單折線圖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
解決vue-router 二級(jí)導(dǎo)航默認(rèn)選中某一選項(xiàng)的問題
今天小編就為大家分享一篇解決vue-router 二級(jí)導(dǎo)航默認(rèn)選中某一選項(xiàng)的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-11-11
vue中如何動(dòng)態(tài)綁定圖片,vue中通過data返回圖片路徑的方法
下面小編就為大家分享一篇vue中如何動(dòng)態(tài)綁定圖片,vue中通過data返回圖片路徑的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-02-02
Vue3使用v-if指令進(jìn)行條件渲染的實(shí)例代碼
條件渲染是根據(jù)條件的真假來有條件地渲染元素,在Vue.js 3.x中,常見的條件渲染包括使用v-if指令和v-show指令,本文講解使用v-if指令進(jìn)行條件渲染,需要的朋友可以參考下2024-03-03
vue如何修改el-form-item中的label樣式修改問題
這篇文章主要介紹了vue如何修改el-form-item中的label樣式修改問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10
vue2.0 與 bootstrap datetimepicker的結(jié)合使用實(shí)例
本篇文章主要介紹了vue2.0 與 bootstrap datetimepicker的結(jié)合使用實(shí)例,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-05-05

