Batch Update 淺析

OlivaRadeck 7年前發布 | 25K 次閱讀 Vue.js 前端技術 Virtual DOM

Virtual DOM 為主流前端 MV* 框架提供了高效的 view 更新機制。即使如此,Virtual DOM 整個 diff/patch 的過程仍然是一個昂貴的操作,在保證 view 及時更新的前提下如何盡可能減少 diff/patch 的次數?這就涉及到 Batch Update 機制。

什么是 Batch Update

Batch Update 即「批量更新」。在 MV* 框架中,Batch Update 可以理解為將一段時間內對 model 的修改批量更新到 view 的機制。以 React 為例,我們在 componentDidMount 生命周期連續調用 setState :

componentDidMount () {
  this.setState({ foo: 1 })
  this.setState({ foo: 2 })
  this.setState({ foo: 3 })
}

在不引入 Batch Update 的情況下,上面的操作會導致三次組件渲染,而實際運行上面的代碼可以發現組件只渲染了一次。componentDidMount 中三次對 model 的操作被 Batch Update 優化為一次 view 的更新,不必要的 Virtual DOM 計算被省略,從而提高了框架的效率。

Batch Update 的實現

我們很容易想到使用一個 queue 來保存 update,并在合適的時候對這個 queue 進行 flush 操作。但在前端框架中實現 Batch Update 的關鍵在于兩個問題:

  1. 何時開始一個 queue
  2. 何時 flush 這個 queue

主流的前端框架都有自己的 Batch Update 實現。以 React 和 Vue 為例,這兩個框架用完全不同思路實現了 Batch Update。

首先是 React:React 中的 Batch Update 是通過「Transaction」實現的。在 React 源碼關于 Transaction 的部分,用 一大段文字及一幅字符畫 解釋了 Transaction 的作用:

*                       wrappers (injected at creation time)
*                                      +        +
*                                      |        |
*                    +-----------------|--------|--------------+
*                    |                 v        |              |
*                    |      +---------------+   |              |
*                    |   +--|    wrapper1   |---|----+         |
*                    |   |  +---------------+   v    |         |
*                    |   |          +-------------+  |         |
*                    |   |     +----|   wrapper2  |--------+   |
*                    |   |     |    +-------------+  |     |   |
*                    |   |     |                     |     |   |
*                    |   v     v                     v     v   | wrapper
*                    | +---+ +---+   +---------+   +---+ +---+ | invariants
* perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
*                    | |   | |   |   |         |   |   | |   | |
*                    | |   | |   |   |         |   |   | |   | |
*                    | |   | |   |   |         |   |   | |   | |
*                    | +---+ +---+   +---------+   +---+ +---+ |
*                    |  initialize                    close    |
*                    +-----------------------------------------+

Transaction 對一個函數進行包裝,讓 React 有機會在一個函數運行前后執行特定邏輯,從而完成整個 Batch Update 流程的控制。

簡單來說,在 Transaction 的 initialize 階段,一個 update queue 被創建。在 Transaction 中調用 setState 方法時,狀態并不會立即應用,而是被推入到 update queue 中。函數執行結束進入 Transaction 的 close 階段,update queue 會被 flush,這時新的狀態會被應用到組件上并開始后續 Virtual DOM 更新等工作。

與 React 相比 Vue 實現 Batch Update 的方法就要簡單很多:直接借助 JavaScript 的 Event Loop。Vue 中 Batch Update 的核心代碼只有大約 20 行:

// https://github.com/vuejs/vue/blob/dev/src/core/observer/scheduler.js#L122-L148
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

當 model 被修改時,對應的 watcher 會被推入 update queue,與此同時還會在異步隊列中添加一個 task 用來 flush 當前 update queue。這樣一來,當前 task 中的其他 watcher 會被推入同一個 update queue 中。當前 task 執行結束后,異步隊列中的下一個 task 會開始執行,update queue 會被 flush,并進行后續的更新操作。

為了讓 flush 動作能在當前 Task 結束后盡可能早的開始,Vue 會優先嘗試將任務 micro-task 隊列,具體來說,在瀏覽器環境中 Vue 會優先嘗試使用 MutationObserver API 或 Promise,如果兩者都不可用,則 fallback 到 setTimeout。

對比兩個框架可以發現 React 基于 Transition 實現的 Batch Query 是一個不依賴語言特性的通用模式,因此有更穩定可控的表現,但缺點是無法完全覆蓋所有情況,例如對于如下代碼:

componentDidMount () {
  setTimeout(_ => {
    this.setState({ foo: 1 })
    this.setState({ foo: 2 })
    this.setState({ foo: 3 })
  }, 0)
}

由于 setTimeout 的回調函數「不受 React 控制」,其中的 setState 就無法得到優化,最終會導致 render 函數執行三次。

而 Vue 的實現則對語言特性乃至運行環境有很強的依賴,但可以更好的覆蓋各種情況:只要是在同一個 task 中的修改都可以進行 Batch Update 優化。

總結

了解 Batch Update 的原理及實現目的是為了幫助我們避開平常代碼中相關的「坑」,同時根據框架的特性來寫出更加高效的代碼。進一步來說,Batch Update 不是框架的專利,我們的許多業務場景也可以使用 Batch Update 的思想進行優化:比如在一些復雜的表單中用戶連續操作之后再進行集中的保存/提交操作,避免頻繁的保存/提交造成資源浪費。

篇幅有限,本文只對 Batch Update 的原理及主流框架中的實現進行了簡單的分析,許多細節(如 update queue 的排重和合并,組件樹的更新順序等等)并沒有一一涉及。希望能對大家的學習有所幫助,也歡迎興趣的同學一起探討。

 

來自:https://zhuanlan.zhihu.com/p/28532725

 

 本文由用戶 OlivaRadeck 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!