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

2024年2月6日 24点热度 0人点赞

寫在前面

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

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

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

  1. 面試官:Vue3響應式系統都不會寫,還敢說精通?
  2. 面試官:你覺得Vue的響應式系統僅僅是一個Proxy?

什麼是調度執行?

什麼是調度執行?

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

來看個例子

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  
}