温馨提示:在看本内容之前最好已经知道vue双向绑定原理,大概了解观察者Watcher的作用是什么,如果不知道可以观看我的关于vue双向绑定原理的文章
概念:nextTick是等待下一次DOM更新刷新的工具方法。
Watcher队列
首先当数据发生变化的时候Dep.notify()会通知观察者Watcher。Watcher就会触发其内部的update()函数执行,下面是update函数的源码
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
/*同步则执行run直接渲染视图*/
this.run()
} else {
/*异步推送到观察者队列中,下一个tick时调用。*/
queueWatcher(this)
}
}
可以看到vue默认使用的是异步DOM更新,当执行update时会调用queueWatcher函数。
queueWatcher函数源码
/*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/
export function queueWatcher (watcher: Watcher) {
/*获取watcher的id*/
const id = watcher.id
/*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
if (has[id] == null) {
has[id] = true
if (!flushing) {
/*如果没有flush掉,直接push到队列中即可*/
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 >= 0 && queue[i].id > watcher.id) {
i--
}
queue.splice(Math.max(i, index) + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
//这里的flushSchedulerQueue的主要目的就是执行Watcher的run函数,用于更新视图。
nextTick(flushSchedulerQueue)
}
}
}
查看queueWatcher的源码我们发现,Watch对象并不是立即更新视图,而是被push进了一个队列queue,此时状态处于waiting的状态,这时候会继续会有Watch对象被push进这个队列queue,等到下一个tick运行时,这些Watch对象才会被遍历取出,更新视图。同时,id重复的Watcher不会被多次加入到queue中去,因为在最终渲染时,我们只需要关心数据的最终结果。
nextTick
补充知识:根据 HTML Standard,在每个 task(宏任务) 运行完以后,UI 都会重渲染,那么在 microtask(微任务) 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了。反之如果新建一个 task 来做数据更新,那么渲染就会进行两次。
vue.js提供了一个[nextTick]函数,其实也就是上面调用的nextTick。
nextTick的实现比较简单,执行的目的是在microtask(微队列)或者task(宏队列)中推入一个function,在当前栈执行完毕(也许还会有一些排在前面的需要执行的任务)以后执行nextTick传入的function。
为什么要进行异步DOM更新的例子:
<template>
<div>
<div>{{test}}</div>
</div>
</template>
export default {
data () {
return {
test: 0
};
},
mounted () {
for(let i = 0; i < 1000; i++) {
this.test++;
}
}
}
现在有这样的一种情况,mounted的时候test的值会被++循环执行1000次。
每次++时,都会根据响应式触发setter->Dep->Watcher->update->patch。
如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。
所以Vue.js实现了一个queue队列,在下一个tick的时候会统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。
也就是说由于Watcher中的更新的操作是在nextTick是放在nextTick函数中的(上面提到的nextTick(flushSchedulerQueue)),而当前执行栈正在执行text++,当该操作执行完毕也就是执行了1000次累加后。才将Watcher中的更新函数放到执行栈中执行,这样的话Watcher获取到的新值就是1000并且只执行一次即可。
而引用nextTick(getNewDate())方法就能获取到更新后的DOM是因为,由于页面数据一旦改变就会触发Watcher,虽然是nextTick(flushSchedulerQueue))但是它排在nextTick(getNewDate())前面的,当更新函数执行完成后,才会执行getNewDate(),因此获取到的就是更新后的DOM
面试回答:说一下nextTick
nextTick是等待下一次DOM更新刷新的工具方法。它的原理是将nextTick中的函数推入到microtask或者task中,只有当前执行栈完成之后才会执行nextTick传入的函数。它的使用场景有两个,首先是vue对于DOM的异步更新策略,当数据发生变化的时候浏览器并不会立刻更新视图而是开启一个队列,将相应的观察者Watcher添加到队列中,并且在该函数内部会将这些Watcher的更新函数放到nextTick中,进行异步批量的处理。(要是举例子就说上面这个循环的例子就行)另外就是为了获取更新后的DOM而使用nextTick方法,原理是这个nextTick中的callback还会被添加到之前说的处理更新函数的后面,因此callback中获取的就是更新后的DOM。