最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Vue.js3.2響應(yīng)式部分的優(yōu)化升級(jí)詳解

 更新時(shí)間:2022年07月04日 09:18:04   作者:黃軼  
這篇文章主要為大家介紹了Vue.js3.2響應(yīng)式部分的優(yōu)化升級(jí)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

背景

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ì)于用戶的使用層面來(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%,寫效率提升約為 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)檫@么核心的代碼能來(lái)自社區(qū)的貢獻(xiàn),這就意味著 Vue 3 受到越來(lái)越多的人關(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 還是選擇了它來(lái)實(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)的 gettersetter,所以它只能監(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ì)象屬性被訪問(wèn)的時(shí)候才會(huì)判斷子屬性的類型來(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ì)象的讀寫,當(dāng)我們?cè)L問(wèn)數(shù)據(jù)時(shí),會(huì)觸發(fā) getter 執(zhí)行依賴收集;修改數(shù)據(jù)時(shí),會(huì)觸發(fā) setter 派發(fā)通知。

接下來(lái),我們簡(jiǎn)單分析一下依賴收集和派發(fā)通知的實(shí)現(xiàn)(Vue.js 3.2 之前的版本)。

依賴收集

首先來(lái)看依賴收集的過(guò)程,核心就是在訪問(wèn)響應(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 表示訪問(wèn)的屬性。

track 函數(shù)外部創(chuàng)建了全局的 targetMap 作為原始數(shù)據(jù)對(duì)象的 Map,它的鍵是 target,值是 depsMap,作為依賴的 Map;這個(gè) depsMap 的鍵是 targetkey,值是 dep 集合,dep 集合中存儲(chǔ)的是依賴的副作用函數(shù)。為了方便理解,可以通過(guò)下圖表示它們之間的關(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) {
  // 通過(guò) 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ù) keydepsMap 中找到對(duì)應(yīng)的 effect 添加到 effects 集合;
  • 遍歷 effects 執(zhí)行相關(guān)的副作用函數(shù)。

因此每次執(zhí)行 trigger 函數(shù),就是根據(jù) targetkey,從 targetMap 中找到相關(guān)的所有副作用函數(shù)遍歷執(zhí)行一遍。

在描述依賴收集和派發(fā)通知的過(guò)程中,我們都提到了一個(gè)詞:副作用函數(shù),依賴收集過(guò)程中我們把 activeEffect(當(dāng)前激活副作用函數(shù))作為依賴收集,它又是什么?接下來(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 中訪問(wèn)了 counter.num,我們希望在執(zhí)行 count 函數(shù)修改 counter.num 值的時(shí)候,能自動(dòng)執(zhí)行 logCount 函數(shù)。

按我們之前對(duì)依賴收集過(guò)程的分析,如果logCountactiveEffect 的話,那么就可以實(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 = 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ù),我們來(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 引用的依賴
      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é)合上述代碼來(lái)看,effect 內(nèi)部通過(guò)執(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 過(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,如果沒有就把 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ù)訪問(wèn) 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 作為依賴,還通過(guò) 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í)候訪問(wèn)了 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í)行之前,如果通過(guò) 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í)行的過(guò)程中重新收集依賴,這個(gè)過(guò)程牽涉到大量對(duì) Set 集合的添加和刪除操作。在許多場(chǎng)景下,依賴關(guān)系是很少改變的,因此這里存在一定的優(yōu)化空間。

為了減少集合的添加刪除操作,我們需要標(biāo)識(shí)每個(gè)依賴集合的狀態(tài),比如它是不是新收集的,還是已經(jīng)被收集過(guò)的。

所以這里需要給集合 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、trackOpBitmaxMarkerBits。

其中 effectTrackDepth 表示遞歸嵌套執(zhí)行 effect 函數(shù)的深度;trackOpBit 用于標(biāo)識(shí)依賴收集的狀態(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 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
        // 超過(guò) maxMarkerBits 則 trackOpBit 的計(jì)算會(huì)超過(guò)最大整形的位數(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ì)比遞歸深度是否超過(guò)了 maxMarkerBits,如果超過(guò)(通常情況下不會(huì))則仍然執(zhí)行老的 cleanup 邏輯,如果沒超過(guò)則執(zhí)行 initDepMarkers 給依賴打標(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)記依賴已經(jīng)被收集
    }
  }
}

initDepMarkers 函數(shù)實(shí)現(xiàn)很簡(jiǎn)單,遍歷 _effect 實(shí)例中的 deps 屬性,給每個(gè) depw 屬性標(biāo)記為 trackOpBit 的值。

接下來(lái)會(huì)執(zhí)行 fn 函數(shù),在就是副作用函數(shù)封裝的函數(shù),比如針對(duì)組件渲染,fn 就是組件渲染函數(shù)。

當(dāng) fn 函數(shù)執(zhí)行時(shí)候,會(huì)訪問(wèn)到響應(yīng)式數(shù)據(jù),就會(huì)觸發(fā)它們的 getter,進(jìn)而執(zhí)行 track 函數(shù)執(zhí)行依賴收集。相應(yīng)的,依賴收集的過(guò)程也做了一些調(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í)候,是通過(guò)執(zhí)行 createDep 方法完成的,此外,在 dep 把前激活的 effect 作為依賴收集前,會(huì)判斷這個(gè) dep 是否已經(jīng)被收集,如果已經(jīng)被收集,則不需要再次收集了。此外,這里還會(huì)判斷這 dep 是不是新的依賴,如果不是,則標(biāo)記為新的。

接下來(lái),我們?cè)賮?lái)看 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)記,來(lái)看它的實(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)被收集過(guò)但不是新的依賴,需要?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)被收集過(guò)但是新的一輪依賴收集沒有被收集的依賴,從 deps 中移除。這其實(shí)就是解決前面舉的需要 cleanup 的場(chǎng)景:在新的組件渲染過(guò)程中沒有訪問(wèn)到的響應(yīng)式對(duì)象,那么它的變化不應(yīng)該觸發(fā)組件的重新渲染。

以上就實(shí)現(xiàn)了依賴收集部分的優(yōu)化,可以看到相比于之前每次執(zhí)行 effect 函數(shù)都需要先清空依賴,再添加依賴的過(guò)程,現(xiàn)在的實(shí)現(xiàn)會(huì)在每次執(zhí)行 effect 包裹的函數(shù)前標(biāo)記依賴的狀態(tài),過(guò)程中對(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ì) refcomputed 等 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) : 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 屬性的 gettersetter。

當(dāng)訪問(wèn)一個(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ì)象。

接下來(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 = _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,來(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)依賴保存到 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,來(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') &amp;&amp; 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ì)有遞歸的情況,通過(guò)這種方式就可以記錄每個(gè)層級(jí)的依賴標(biāo)記情況。

在判斷某個(gè) dep 是否已經(jīng)被依賴收集的時(shí)候,使用了 wasTracked 函數(shù):

const wasTracked = (dep) => (dep.w & trackOpBit) > 0

通過(guò)與運(yùn)算的結(jié)果是否大于 0 來(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è)依賴。顯然,這個(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ù)的訪問(wèn)和修改都是非常頻繁的操作,因此對(duì)這個(gè)過(guò)程的性能優(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 工具 來(lái)驗(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)文章

最新評(píng)論

龙南县| 姚安县| 孝昌县| 凌源市| 义马市| 安新县| 武鸣县| 武强县| 怀集县| 长治县| 德安县| 彭阳县| 太仆寺旗| 阿拉尔市| 马公市| 舒城县| 温州市| 万宁市| 错那县| 克拉玛依市| 通海县| 墨脱县| 化德县| 曲麻莱县| 山西省| 新龙县| 陕西省| 花莲县| 滕州市| 星子县| 德清县| 赣榆县| 邵武市| 平陆县| 台南市| 贞丰县| 壤塘县| 大埔县| 镇康县| 靖西县| 洮南市|