面試官:說說 Vue3 批量異步更新是如何實現的?

寫在前面

這是 Vue3 源碼分析的第三篇,與響應式系統中調度執行有關,其中 computedwatch 等核心功能都離不開它,可見其重要程度。

除了實現可調度性,我們還會藉助它來實現 vue 中一個非常重要的功能,批量更新或者叫異步更新

多次修改數據 (例如自身 num10 次),只進行一次頁面渲染(頁面只會渲染最後一次 num10)。

什麼是調度執行?

什麼是調度執行?

指的是響應式數據發生變化出發副作用函數重新執行時,我們有能力去決定副作用函數的執行時機次數方式

來看個例子

const state = reactive({
  num: 1
})

effect(() ={
  console.log('num', state.num)
})

state.num++

console.log('end')

如果我們想要它按照這個順序書序呢?

1
end
2

你可能會說,我調換一下代碼順序就好了哇!!!

const state = reactive({
  num: 1
})

effect(() ={
  console.log('num', state.num)
})

console.log('end')

state.num++

淫才啊!😄 瞬間就解決了問題。不過看起來這不是我們想要最終答案。

我們想要通過實現可調度性來解決這個問題。

如何實現可調度?

我們從結果出發來思考如何實現可調度的特性。

const state = reactive({
  num: 1
})

effect(() ={
  console.log(state.num)
}{
  // 注意這裏,假如num發生變化的時候執行的是scheduler函數
  // 那麼end將會被先執行,因爲我們用setTimeout包裹了一層fn
  scheduler (fn) {
    // 異步執行
    setTimeout(() ={
      fn()
    }, 0)
  }
})

state.num++

console.log('end')

看到這裏也許你已經明白了,我們將通過 scheduler 來自主控制副作用函數的執行時機。

在這之前,執行state.num++之後,console.log(state.num)將會被馬上執行,而添加 scheduler 後,num 發生變化後將執行 scheduler 中的邏輯。

源碼實現

雖然可調度性在 Vue 中非常重要,但實現這個機制卻非常簡單,我們甚至只要增加兩行代碼就可以搞定。

第一行代碼

// 增加options參數
const effect = function (fn, options = {}) {
  const effectFn = () ={
   // ....
  }
  // ...
  // 將options參數掛在effectFn上,便於effectFn執行時可以讀取到scheduler
  effectFn.options = options
}

第二行代碼

function trigger(target, key) {
// ...

  effectsToRun.forEach((effectFn) ={
    // 當指定了scheduler時,將執行scheduler而不是註冊的副作用函數effectFn
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
}

是不是簡單到離譜?

批量更新 & 異步更新

來看段詭異的代碼,請問 num 會被執行多少次?100 還是 101?

const state = reactive({
  num: 1
})

effect(() ={
  console.log('num', state.num)
})

let count = 100

while (count--) {
  state.num++
}

對於頁面渲染來說 1 到 101 中間的 2~100 僅僅只是過程,並不是最終的結果,處於性能考慮 Vue 只會渲染最後一次的 101。

Vue 是如何做到的呢?

利用可調度性,再加點事件循環的知識,我們就可以做到這件事。

  1. num 的每次變化都會導致 scheduler 的執行,並將註冊好的副作用函數存入 jobQueue 隊列,因爲 Set 本身的去重性質,最終只會存在一個 fn

  2. 利用 Promise 微任務的特性,當 num 被更改 100 次之後同步代碼全部執行結束後,then 回調將會被執行,此時 num 已經是 101,而 jobQueue 中也只有一個 fn,所以最終只會打印一次 101

 const state = reactive({
  num: 1
})

const jobQueue = new Set()
const p = Promise.resolve()
let isFlushing = false

const flushJob = () ={
  if (isFlushing) {
    return
  }

  isFlushing = true
  // 微任務
  p.then(() ={
    jobQueue.forEach((job) => job())
  }).finally(() ={
    // 結束後充值設置爲false
    isFlushing = false
  })
}

effect(() ={
  console.log('num', state.num)
}{
  scheduler (fn) {
    // 每次數據發生變化都往隊列中添加副作用函數
    jobQueue.add(fn)
    // 並嘗試刷新job,但是一個微任務只會在事件循環中執行一次,所以哪怕num變化了100次,最後也只會執行一次副作用函數
    flushJob()
  }
})

let count = 100

while (count--) {
  state.num++
}

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/InHWMRXBD-XrV0GVRjAowA