Vue的批量更新原理

简单的代码开始:
var app = new Vue({el: '#app',data: {message: 'Hello Vue!'},watch: {message (val) {console.log('mutation')}},mounted () {this.message = 1this.message = 2this.message = 3}})
首先看这个例子中,连续3次触发了mutation,那么watch中的cb会被执行几次呢?
答案是一次。
那么为什么会是一次呢?本文会围绕着这个问题的解释来粗浅地讨论一下Vue中批量更新的原理。
首先要知道,msg这个key,是通过Object.defineProperty被监听了的,Vue通过这个api实现在key被set的时候(也就是this.msg = xxx这种操作),触发所有订阅了这个key的Watcher的update方法。
这里引入了一个Watcher的概念,那么这个Watcher是什么呢?
从语义上理解,Wathcer其实类似于一个key的观察者,当key被set的时候,Watcher会调用自身的update方法。
在 mountComponent 也就是加载组件的时候会调用 Watcher 创建观察者。
new Watcher(vm, updateComponent, noop, {before () {if (vm._isMounted && !vm._isDestroyed) {callHook(vm, 'beforeUpdate')}}}, true /* isRenderWatcher */)
监听到一个set引起的mutation就立即同步执行一次cb吗?
显然是不可以的。
举个例子:
for (let i = 0; i < 1000; i++) {this.msg = `循环了${i}次`;}
假如对于这种操作,1000次里面每次Watcher都要响应执行一次update,那可能是有很大的性能开销的。像本文的这个例子中update其实就是一个console.log,但是如果cb是一个开销比较大的方法,那么就可能会引起性能问题了。
所以update操作一定是异步的。
虽然知道要通过异步来解决,但具体是如何解决的呢?Vue的做法是把调用cb放到了一个micro task或者macro task队列中,具体放到微任务队列还是宏任务队列要看当前的运行环境是否支持Promise、MutationObserver、setImmediate这几个相当于放入微任务队列的api,支持就会放在微任务队列,不支持则使用setTimeout这个api把调用cb放到宏任务队列里。
不管放到微任务队列还是宏任务队列,调用cb都会在所有的同步代码执行完毕后执行。这一点涉及到event loop的知识,因为总是先执行所有的同步代码,然后从微任务队列中按顺序执行,微任务队列空了才会从宏任务队列中取出一条执行。如果此时微任务队列还有任务,那么就会继续按照这个循环执行,这个就是event loop。
通俗地理解Vue的行为就是在监听到key的mutation之后key的Watcher都会触发update,想要调用自身的cb属性,但是Vue仅仅是答应会在未来的某个时刻执行Watcher的这个update请求也就是调用它的cb,并且在调用之前都不会再受理该Watcher的update的请求。
下面看看源码
function queueWatcher (watcher) {const id = watcher.idif (has[id] == null) {has[id] = trueif (!flushing) {queue.push(watcher)} else {let i = queue.length - 1while (i > index && queue[i].id > watcher.id) {i--}queue.splice(i + 1, 0, watcher)}if (!waiting) {waiting = trueif (process.env.NODE_ENV !== 'production' && !config.async) {flushSchedulerQueue()return}nextTick(flushSchedulerQueue)}}}
重点是 nextTick 源码
let timerFuncif (typeof Promise !== 'undefined' && isNative(Promise)) {const p = Promise.resolve()timerFunc = () => {p.then(flushCallbacks)if (isIOS) setTimeout(noop)}isUsingMicroTask = true} else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||MutationObserver.toString() === '[object MutationObserverConstructor]')) {let counter = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {counter = (counter + 1) % 2textNode.data = String(counter)}isUsingMicroTask = true} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {timerFunc = () => {setImmediate(flushCallbacks)}} else {timerFunc = () => {setTimeout(flushCallbacks, 0)}}function nextTick (cb, ctx) {let _resolvecallbacks.push(() => {if (cb) {try {cb.call(ctx)} catch (e) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})if (!pending) {pending = truetimerFunc()}if (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}}
