Vue的批量更新原理
共 3498字,需浏览 7分钟
·
2021-08-12 18:17
简单的代码开始:
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
watch: {
message (val) {
console.log('mutation')
}
},
mounted () {
this.message = 1
this.message = 2
this.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.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
重点是 nextTick 源码
let timerFunc
if (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 = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.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 _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}