首先抛出个问题
下面的打印结果是什么呢?
<body>
<div id="app">
<h1>异步更新</h1>
<p id="p1">{{foo}}</p>
</div>
</body>
<script>
const app = new Vue({
el: "#app",
data: {
foo: 'ready~~~',
},
mounted() {
this.foo = 1;
console.log('1', this.foo)
this.foo = 2
console.log('2', this.foo)
this.foo = 3
console.log('3', this.foo)
this.$nextTick(() => {
console.log('p1.innerHTML:' + p1.innerHTML)
})
},
});
</script>
几乎不用考虑,答案就可以脱口而出
那么我想问一下视图会更新几次呢?貌似只更新了一次,什么原因呢?下面我们带着问题来探讨一下。
数据劫持
我们都知道Object.defineProperty会对data中的数据劫持,当修改数据的时候会触发set方法,同时set会执行dep.notify(), dep.notify()会遍历subs(也就是我们常说的收集依赖的数组)的update方法,也就是触发watcher的update。(以上的一系列操作可以到源码中进行调试就很清晰了, 后续会对熟数据劫持做更加清晰的描述)。
上面描述了那么多,其实就是当数据发生变化了触发set方法让相关的依赖(Watcher)去执行update方法。
那update都干了什么呢?
// 路径: src\core\observer\watcher.js
update () {
/* istanbul ignore else */
// 计算属性
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// watcher入队
queueWatcher(this)
}
}
Watcher进入队列发生的事情
lazy在计算属性中会用到,sync表示同步,我们这里不做讲解。
主要的一段代码在queueWatcher,让当前的watcher进入一个队列当中。
// 路径: src\core\observer\scheduler.js
export function queueWatcher (watcher: Watcher) {
// 获取watcher的id
const id = watcher.id
// 判断是否已经入队,去重
if (has[id] == null) {
has[id] = true
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.
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
// 如果没有在等待状态
// 使用nextTick将flushSchedulerQueue入队
// 尝试异步的方式将该函数放入微任务队列
nextTick(flushSchedulerQueue)
}
}
}
首先判断Watcher是否已经加入了队列当中,如果入了队列则不做操作。所以刚开始我们的问题中,执行了三次修改了数据,最后只会有一个Watcher被加入队列(感觉有那么点意思了)。接着去执行nextTick将flushSchedulerQueue入队,那么flushSchedulerQueue又是干什么的呢?
// 路径:src\core\observer\scheduler.js
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) => 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
// 执行run函数
watcher.run()
}
}
这里我们把代码简化了,你可以理解为flushSchedulerQueue就是去让队列中的watcher执行run方法去更新视图的。当然这里我们只是把flushSchedulerQueue传给了nextTick,让其入队。
nextTick
接下来我们来看看nextTick方法:
// 路径: src\core\util\next-tick.js
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 用户传递的回调函数会被放入callbacks里面
// 前面的刷新函数就是执行callbacks中的所有回调函数
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
}
只是把我们传入的flushSchedulerQueue加入了callbacks数组中,但是我们有可能使用this.$nextTick(cb),所以cb也会被将入callbacks,然后看下当时是否需要等待,如果不需要则执行timerFunc()
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
}
异步更新
这里我们只讲解promise,我们可以看到上面的代码就是将flushCallbacks异步执行,flushCallbacks就是执行我们提到的callbacks数组中的所有的回调函数。按照我们刚开始写得代码,此时的callbacks中应该有两个回调,一个是执行watcher.run()的flushSchedulerQueue,还有一个是this.$nextTick中传入的回调。回忆刚刚的问题,为什么视图只更新了一次?当执行watcher.run()的时候,this.foo已经3了,所以只会更新一次。
不知道是否理解了呢?欢迎留言哦
最后来个流程图,希望能帮助大家理解。