Batch Update 浅析
OlivaRadeck
7年前
<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>