Batch Update 浅析

OlivaRadeck 4年前
   <p style="text-align: center;"><img src="https://simg.open-open.com/show/d8191adbe2f7f33c62673cd06c00d512.png"></p>    <p>Virtual DOM 为主流前端 MV* 框架提供了高效的 view 更新机制。即使如此,Virtual DOM 整个 diff/patch 的过程仍然是一个昂贵的操作,在保证 view 及时更新的前提下如何尽可能减少 diff/patch 的次数?这就涉及到 Batch Update 机制。</p>    <h2><strong>什么是 Batch Update</strong></h2>    <p>Batch Update 即「批量更新」。在 MV* 框架中,Batch Update 可以理解为将一段时间内对 model 的修改批量更新到 view 的机制。以 React 为例,我们在 componentDidMount 生命周期连续调用 setState :</p>    <pre>  <code class="language-javascript">componentDidMount () {    this.setState({ foo: 1 })    this.setState({ foo: 2 })    this.setState({ foo: 3 })  }</code></pre>    <p>在不引入 Batch Update 的情况下,上面的操作会导致三次组件渲染,而实际运行上面的代码可以发现组件只渲染了一次。componentDidMount 中三次对 model 的操作被 Batch Update 优化为一次 view 的更新,不必要的 Virtual DOM 计算被省略,从而提高了框架的效率。</p>    <h2><strong>Batch Update 的实现</strong></h2>    <p>我们很容易想到使用一个 queue 来保存 update,并在合适的时候对这个 queue 进行 flush 操作。但在前端框架中实现 Batch Update 的关键在于两个问题:</p>    <ol>     <li>何时开始一个 queue</li>     <li>何时 flush 这个 queue</li>    </ol>    <p>主流的前端框架都有自己的 Batch Update 实现。以 React 和 Vue 为例,这两个框架用完全不同思路实现了 Batch Update。</p>    <p>首先是 React:React 中的 Batch Update 是通过「Transaction」实现的。在 React 源码关于 Transaction 的部分,用 <a href="/misc/goto?guid=4959751569668196597" rel="nofollow,noindex"> 一大段文字及一幅字符画 </a> 解释了 Transaction 的作用:</p>    <pre>  <code class="language-javascript">*                       wrappers (injected at creation time)  *                                      +        +  *                                      |        |  *                    +-----------------|--------|--------------+  *                    |                 v        |              |  *                    |      +---------------+   |              |  *                    |   +--|    wrapper1   |---|----+         |  *                    |   |  +---------------+   v    |         |  *                    |   |          +-------------+  |         |  *                    |   |     +----|   wrapper2  |--------+   |  *                    |   |     |    +-------------+  |     |   |  *                    |   |     |                     |     |   |  *                    |   v     v                     v     v   | wrapper  *                    | +---+ +---+   +---------+   +---+ +---+ | invariants  * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained  * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->  *                    | |   | |   |   |         |   |   | |   | |  *                    | |   | |   |   |         |   |   | |   | |  *                    | |   | |   |   |         |   |   | |   | |  *                    | +---+ +---+   +---------+   +---+ +---+ |  *                    |  initialize                    close    |  *                    +-----------------------------------------+</code></pre>    <p>Transaction 对一个函数进行包装,让 React 有机会在一个函数运行前后执行特定逻辑,从而完成整个 Batch Update 流程的控制。</p>    <p>简单来说,在 Transaction 的 initialize 阶段,一个 update queue 被创建。在 Transaction 中调用 setState 方法时,状态并不会立即应用,而是被推入到 update queue 中。函数执行结束进入 Transaction 的 close 阶段,update queue 会被 flush,这时新的状态会被应用到组件上并开始后续 Virtual DOM 更新等工作。</p>    <p>与 React 相比 Vue 实现 Batch Update 的方法就要简单很多:直接借助 JavaScript 的 Event Loop。Vue 中 Batch Update 的核心代码只有大约 20 行:</p>    <pre>  <code class="language-javascript">// 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)      }    }  }</code></pre>    <p>当 model 被修改时,对应的 watcher 会被推入 update queue,与此同时还会在异步队列中添加一个 task 用来 flush 当前 update queue。这样一来,当前 task 中的其他 watcher 会被推入同一个 update queue 中。当前 task 执行结束后,异步队列中的下一个 task 会开始执行,update queue 会被 flush,并进行后续的更新操作。</p>    <p>为了让 flush 动作能在当前 Task 结束后尽可能早的开始,Vue 会优先尝试将任务 micro-task 队列,具体来说,在浏览器环境中 Vue 会优先尝试使用 MutationObserver API 或 Promise,如果两者都不可用,则 fallback 到 setTimeout。</p>    <p>对比两个框架可以发现 React 基于 Transition 实现的 Batch Query 是一个不依赖语言特性的通用模式,因此有更稳定可控的表现,但缺点是无法完全覆盖所有情况,例如对于如下代码:</p>    <pre>  <code class="language-javascript">componentDidMount () {    setTimeout(_ => {      this.setState({ foo: 1 })      this.setState({ foo: 2 })      this.setState({ foo: 3 })    }, 0)  }</code></pre>    <p>由于 setTimeout 的回调函数「不受 React 控制」,其中的 setState 就无法得到优化,最终会导致 render 函数执行三次。</p>    <p>而 Vue 的实现则对语言特性乃至运行环境有很强的依赖,但可以更好的覆盖各种情况:只要是在同一个 task 中的修改都可以进行 Batch Update 优化。</p>    <h2><strong>总结</strong></h2>    <p>了解 Batch Update 的原理及实现目的是为了帮助我们避开平常代码中相关的「坑」,同时根据框架的特性来写出更加高效的代码。进一步来说,Batch Update 不是框架的专利,我们的许多业务场景也可以使用 Batch Update 的思想进行优化:比如在一些复杂的表单中用户连续操作之后再进行集中的保存/提交操作,避免频繁的保存/提交造成资源浪费。</p>    <p>篇幅有限,本文只对 Batch Update 的原理及主流框架中的实现进行了简单的分析,许多细节(如 update queue 的排重和合并,组件树的更新顺序等等)并没有一一涉及。希望能对大家的学习有所帮助,也欢迎兴趣的同学一起探讨。</p>    <p> </p>    <p>来自:https://zhuanlan.zhihu.com/p/28532725</p>    <p> </p>