前言
?首先我们了解到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进行更新,源码如下:
Watcher.prototype.update = function update() {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
?从这里我们可以发现vue默认就是走的是异步更新机制,默认调用queueWatcher方法将当前这个watcher加入到异步队列中,下面我们看一下queueWatcher方法:
function queueWatcher(watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
if (!waiting) {
waiting = true;
if (!config.async) {
flushSchedulerQueue();
return
}
nextTick(flushSchedulerQueue);
}
}
}
?通过这段代码我们就可以发现,当调用queueWatcher时会判断当前watcher是否已经添加在队列中,没有添加到队列中才会继续执行,然后判断当flushing标志锁为false,说明flushSchedulerQueue(刷新watcher队列)还没有执行,所以仍然可以继续将watcher添加到队列中,当已经执行刷新队列时,那么就将watcher通过id进行排序插入到合适的位置。当waiting这个标志锁为true时,说明正在执行刷新队列,则跳过执行,当为false的时候,说明没有执行刷新队列则调用nextTick去执行此方法,对应nextTick原理,可以看这篇文章Vue源码学习之nextTick,下面我们分析下flushSchedulerQueue的源码:
function flushSchedulerQueue() {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
queue.sort(function (a, b) {
return a.id - b.id;
});
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
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
}
}
}
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
resetSchedulerState();
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
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源码系列文章:
|