在Vue的源码中,当响应式数据发现改变的时候并不会立刻更新页面,而是执行执行queueWatcher函数,而这个函数内部维护着一个队列,而需要执行更新的组件会被推入到这个队列中,如果你在一个事件循环多次向这个队列中push同一个组件,它只会push一次,因为它每次push的时候都会拿你正在push的组件的id去跟队列中的组件的id比较,如果存在就不再push。Vue会把队列通过nextTick来执行,而nextTick也不是立刻执行的,它内部是以Promise,mutationObserver,setImmediate,setTimeout其中一个来执行,优先级就是上面的排列顺序,所以它就变成了一个异步任务。因此子组件只会更新一次。
Vue视图更新源码分析如下:
/**
*将观察程序推入观察程序队列。
*ID重复的作业将被跳过,除非
*在刷新队列时推送。
*/
export function queueWatcher(watcher: Watcher) {
// 首先获取当前 watcher 的 id
const id = watcher.id
// 判断这个 watcher 是否已经存在,因为 Vue 并不是在数据发生变化之后立刻更新视图
// 而是收集起来的,因此在上一次没有更新视图之前,即使数据发生改变也无需再添加,因为
// watcher 更新视图的时候使用的数据依然是最新的数据
if (has[id] != null) {
// 已经存在就不再收集
return
}
if (watcher === Dep.target && watcher.noRecurse) {
return
}
has[id] = true
if (!flushing) {
// 否则把 watcher 放到一个队列中,在下一次事件循环的时候再一次性更新视图;
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 > index && queue[i].id > watcher.id) {
i-- }
queue.splice(i + 1, 0, watcher)
}
// queue the flush
// 接着就开始执行渲染
if (!waiting) {
waiting = true
if (__DEV__ && !config.async) {
flushSchedulerQueue()
return
}
// 执行渲染,可以看到它调用的就是 nextTick 方法;
// flushSchedulerQueue 就是刷新页面的
nextTick(flushSchedulerQueue)
}
}
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
//刷新前对队列进行排序。
//这样可以确保:
//1。组件从父级更新到子级。(因为父母总是
//在子项之前创建)
//2。组件的用户观察程序在其渲染观察程序之前运行(因为
//用户观察程序是在渲染观察程序之前创建的)
//3。如果组件在父组件的观察程序运行期间被破坏,
//它的观察者可以被跳过。
queue.sort(sortCompareFn)
//不要缓存长度,因为可能会推送更多的观察者
//当我们运行现有的观察程序时
for (index = 0; index 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
}
}
}