前言
首先我们了解到Vue的Dom更新是异步的,当我们更新数据后,立即获取Dom的内容,此时Dom的内容还是旧的内容,那么我们可以通过$nextTick在回调函数中去获取最新的Dom内容,那么这个时候就有所考虑了,为什么就有所考虑了,为什么我们不在Vue中所操作的Dom是同步的,在Vue中确是异步的呢?实际跟Vue的渲染机制有关,在Vue中当修改数据后,此时渲染是异步的,所以Dom的更新也是为异步的。
Dom的更新是批量的?
Vue中Dom的更新不仅仅是异步的而且还是批量的,这样做的好处是能够最大程度的优化性能,对于相同Dom的多次修改,我们只需要赋值最后一次修改的结果即可,演示如下:
<template>
<div class="hello">
<span>名字:{
{
name }}</span>
<span> | </span>
<span>年龄:{
{
age }}</span>
<span> | </span>
<span>渲染次数:{
{
updateCount }}</span>
</div>
</template>
<script>
export default {
name: "HelloWorld",
data() {
return {
name: "小飞",
age: 18,
updateCount: 0,
};
},
mounted() {
this.name = "小哲";
this.age = 30;
},
updated() {
this.updateCount++;
},
};
</script>
由此可以看出,当我们对两种数据进行修改时,updated钩子函数只会执行一次,也就我们同时更新了多个数据,Dom只会更新一次。
源码分析
异步更新队列
在Vue中DOM更新一定是由于数据变化引起的,当数据变化时,会触发数据劫持中的setter,使其调用dep实例的notify方法,通知所有订阅者watcher进行更新,源码如下:
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
Watcher.prototype.update = function update() {
// 懒执行会走这里, 比如computed
if (this.lazy) {
this.dirty = true;
// 同步执行会走这里,比如this.$watch() 或watch选项,传递一个sync配置{sync: true}
} else if (this.sync) {
this.run();
} else {
// 将当前watcher添加到watcher队列中,默认走此else
queueWatcher(this);
}
};
从这里我们可以发现vue默认就是走的是异步更新机制,默认调用queueWatcher方法将当前这个watcher加入到异步队列中,下面我们看一下queueWatcher方法:
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
function queueWatcher(watcher) {
var id = watcher.id;
// 判断watcher是否被标记过,如果标记过则不执行,避免重复推入watcher
if (has[id] == null) {
// 没被标记过的watcher则进行标记
has[id] = true;
// 如果flushing为false, 表示当前watcher队列没有在被刷新,则watcher直接进入队列
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.
// 如果watcher队列已经在刷新了,这个时候我们插入对应的watcher时,就需要对即将插入的watcher进行排序插入
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
// 当waiting为true时,代表flushSchedulerQueue正在执行,当执行完毕后waiting会更新
if (!waiting) {
waiting = true;
if (!config.async) {
flushSchedulerQueue();
return
}
// 将flushSchedulerQueue放在下一个事件循环中进行执行
nextTick(flushSchedulerQueue);
}
}
}
通过这段代码我们就可以发现,当调用queueWatcher时会判断当前watcher是否已经添加在队列中,没有添加到队列中才会继续执行,然后判断当flushing标志锁为false,说明flushSchedulerQueue(刷新watcher队列)还没有执行,所以仍然可以继续将watcher添加到队列中,当已经执行刷新队列时,那么就将watcher通过id进行排序插入到合适的位置。当waiting这个标志锁为true时,说明正在执行刷新队列,则跳过执行,当为false的时候,说明没有执行刷新队列则调用nextTick去执行此方法,对应nextTick原理,可以看这篇文章Vue源码学习之nextTick,下面我们分析下flushSchedulerQueue的源码:
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue() {
currentFlushTimestamp = getNow();
// 将flushing设置为true(标志锁)
flushing = true;
var watcher, id;
// flush队列前先排序
// 目的是
// 1. Vue中的组件的更新是从父组件到子组件(因为父组件总是比子组件先创建)
// 2. user watcher比render watcher执行要早(因为user watcher比render watcher创建要早一些)
// 3. 如果父组件的watcher调用run时将父组件干掉了,那其子组件的watcher也就没必要调用了
// 通过watcher id进行排序
queue.sort(function (a, b) {
return a.id - b.id;
});
// 依次执行watcher的run方法,进行更新dom
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
// 更新dom
watcher.run();
// dev环境下,检测是否为死循环
if (has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user ?
("in watcher with expression \"" + (watcher.expression) + "\"") :
"in a component render function."
),
watcher.vm
);
break
}
}
}
// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
// 刷新flushing为false,waiting也为false,便于下次开启队列
resetSchedulerState();
// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
首先先将flushing变为true,说明已经开始执行刷新队列了,那么后续如果在继续添加watcher的话,则使用watcher id对其排序添加到队列中,然后依次执行队列中watcher的run方法进行更新dom(diff更新),当刷新完毕后执行resetSchedulerState方法,将waiting和flushing标志锁变为false,便于下次开启队列,那么由此我们可以得出,当数据更新时,会将对应的watcher添加到异步队列中,在下一个事件循环中去执行刷新队列的方法,进行Dom更新。
总结
当数据被赋值变化时,会触发dep实例的notify方法,使其依次调用watcher的update方法,在update方法中会将其watcher加入到异步队列中,并且无法重复加入相同的watcher,将刷新队列的方法通过nextTick使其在下一个事件循环中进行执行,刷新队列时,依次调用watcher的run方法进行更新dom。
Vue源码系列文章: