「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」
前言
在开讲之前,大家先要保证自己明白什么是js事件循环机制了,即event loop
参考文章:Tasks, microtasks, queues and schedules
作者:Jake
中文的大家自行百度,很多
场景一:
<template>
<div>
<p id="p1">{{foo}}</p>
</div>
</template>
new Vue({
data(){
return {
foo:0
}
},
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);
})
}
})
// 输出结果
// 1:1
// 2:2
// 3:3
// p1.innerHTML:3
过程分析:
this.foo = 1,触发了哪些事情?如下:
- this.foo = 1,修改了foo的值,触发foo响应式数据的set拦截方法
- set方法里触发dep.notify()去通知对应的watcher更新视图
- 对应的watcher调用自身的update方法
- update方法里调用queueWatcher(this)
- queueWatcher(this)方法,将watcher自身加入一个watcher队列queue,并执行vue内部的nextTick(flushSchedulerQueue)方法, 让flushSchedulerQueue在未来某个时刻去执行。
- flushSchedulerQueue是一个任务冲刷函数,逻辑是循环调用watcher队列queue里每个watcher的run()方法去更新视图
- nextTick方法将上一步的flushSchedulerQueue函数加入到另一个数组callbacks里,并且执行timerFunc(),启动异步任务刷新流程,且设置变量pending = true,该变量只有等后面开始调用flushcallbacks之后,才会再次置为false。
- timerFunc()会根据浏览器环境判断 promise > MutationObserver > setImmediate > setTimeout,优先使用promise去执行promise.resolve().then(flushcallbacks),来将冲刷任务数组的方法flushcallbacks加入到浏览器的微任务队列miroTasks 中,
即miroTasks = [flushcallbacks]
- flushcallbacks是针对第7点的callbacks数组的冲刷方法,会遍历callbacks数组,执行里面的每一项flushSchedulerQueue。但flushcallbacks在浏览器的微任务队列里,不会马上执行,会等当前这一次的同步代码执行完,才会执行,所以代码会继续往下走
this.foo = 2,触发了哪些事情?如下:
1、2、3、4步同上 5. queueWatcher(this)方法,判断当前watcher已经在任务队列中了,所以不会让watcher如下,后面的步骤不走了
this.foo = 3,触发了哪些事情?如下:
同上
this.$nextTick(cb),触发了哪些事情?如下:
-
执行vue提供的$nextTick方法(同上面的nextTick方法,只是一个在内部vue自己调用,一个给用户调用),将cb放入到callbacks数组中。
- 那么此时callbacks = [flushSchedulerQueue,cb]
-
由于我们在this.foo = 2的时候,第6步里面,设置了pending = true,所以这里并不会再次执行timeFunc()
到这里,同步代码执行结束
接下来处理微任务
- 浏览器会扫描微任务队列,发现微任务队列miroTasks = [flushcallbacks],
- 将微任务队列中的flushcallbacks拿出来执行,
- 这时候会去遍历数组callbacks = [flushSchedulerQueue,cb],
- 先执行flushSchedulerQueue,遍历watcher数组queue,执行每个watcher的watcher.run()方法。
- 这里watcher后面的大该过程不是我们这里讨论的重点,我们只要知道watcher后面执行的逻辑直到dom更新都是同步的,大该流程如下: watcher.run() => watcher.get() => watcher.getter() => 触发 new Watcher(vm,updateComponent)时传入的updateComponent() => vm._render()构建vNode => vm._update => patch比较 => 挂载 => 真正dom更新
- 到这里,dom已经更新了。此时callbacks里只剩下cb了,即 callbacks = [cb]
- 执行cb,也就是我们用户调用this.$nextTick传入的回调,所以打印出来的p1.innerHTML为dom更新后的值即p1.innerHTML:3
场景二:
<template>
<div>
<p id="p1">{{foo}}</p>
</div>
</template>
new Vue({
data(){
return {
foo:0
}
},
mounted(){
this.foo = 1
console.log('1:' + this.foo);
this.foo = 2
console.log('2:' + this.foo);
this.$nextTick(() => {
console.log('p1.innerHTML:' + p1.innerHTML);
})
this.foo = 3
console.log('3:' + this.foo);
}
})
// 输出结果
// 1:1
// 2:2
// 3:3
// p1.innerHTML:3
很多朋友这里可能会说,因为this.$nextTick是异步的,所以回调最后执行,console.log出来的就是dom更新之后的结果, p1.innerHTML:3
这话其实只数对了一半,为什么呢,请看下的例子
场景三:
<template>
<div>
<p id="p1">{{foo}}</p>
</div>
</template>
new Vue({
data(){
return {
foo:0
}
},
mounted(){
this.$nextTick(() => {
console.log('p1.innerHTML:' + p1.innerHTML);
})
this.foo = 1
console.log('1:' + this.foo);
this.foo = 2
console.log('2:' + this.foo);
this.foo = 3
console.log('3:' + this.foo);
}
})
// 输出结果
// 1:1
// 2:2
// 3:3
// p1.innerHTML:1
不相信的朋友的,可以去自己的vue demo里试一下,看看是不是如我所说。
为什么会这样呢? $nextTick不是会等dom更新再执行吗? 为什么输出的结果dom更新前的呢?
我们来结合场景一的步骤分析以下:
- 用户执行this.$nextTick(cb)方法,会将cb放进我们刚才场景一步骤6的callbacks数组里,也就是此时callbacks = [cb]。并且由于此时callbacks没有开始被冲刷,所以pending = false,所以我们会去执行timeFunc(),且将pending置为true。
- 执行timeFunc(),将flushCallbacks()函数放进微任务队列里,此时 microTasks = [flushCallbacks]
- this.foo = 1,会触发watcher那一系列的操作,从步骤1 -> 步骤6,得到的结果就是,将watcher放入任务队列queue = [watcher],然后将任务冲刷函数flushSchedulerQueue放入callbacks, 也就是此时callbacks = [cb, flushSchedulerQueue],然后由于此时pending = true,所以不会再次执行timeFunc()。
- 此时浏览器会继续执行同步任务,直到此轮同步任务执行完,再去执行微任务队列
- this.foo = 2和this.foo = 3,我们场景一分析过,queueWatcher会对watcher去重,所以此时watcher并不会放入到任务队列
- 同步代码执行完成,开始执行微任务。
- 浏览器拿出微任务队列microTasks = [flushCallbacks],执行flushCallbacks
- 遍历callbacks = [cb, flushSchedulerQueue],先执行cb
- 此时先执行的是cb,而我们知道dom更新是在flushSchedulerQueue任务冲刷里操作的,所以此时dom并没有更新,所以cb打印出来的值是dom更新前的,即p1.innerHTML:1
所以,这里我们可以总结一个很重要的结论
this.$nextTick(cb)会让回调在dom更新之后执行,但是~~
必须在this.$nextTick(cb)之前已经有过对数据修改。
因为这个修改会触发了vue内的一系列操作,让内部的nextTick执行。进而将更新视图的任务冲刷函数flushSchedulerQueue放入到callbacks数组中,以供后序调用。
那么,大家是不是胸有成竹了,我们再来看最后一个例子
场景四:
问promise和nextTick执行顺序是怎样的,输出值多少?
<template>
<div>
<p id="p1">{{foo}}</p>
</div>
</template>
new Vue({
data(){
return {
foo:0
}
},
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);
Promise.resolve().then(() => {
console.log('promise:' + p1.innerHTML)
})
this.$nextTick(() => {
console.log('p1.innerHTML:' + p1.innerHTML);
})
}
})
// 输出结果
// 1:1
// 2:2
// 3:3
// p1.innerHTML:3
// promise:3
小伙伴们答对了吗? 是不是觉得Promise.resolve().then不是微任务吗? this.$nextTick也是微任务,promise应该先被推入队列去执行才对呀?
我们来看看为什么
- this.foo = 1, 触发了一系列操作,包括vue内部的nextTick方法,并执行了timeFunc()。
- 此时watcher队列为,queue = [watcher],
- callbacks数组为callbacks = [flushSchedulerQueue]
- 微任务队列为,microTasks = [flushCallbacks]
- this.foo = 2,和this.foo = 3我们就不说了,上面分析过
- 执行Promise.resolve().then(promiseCb),将回调函数promiseCb加入到微任务队列中
- 此时微任务队列为,microTasks = [flushCallbacks,promiseCb]
- 用户执行this.$nextTick(nextTickCb),将回调函数nextTickCb加入到callbacks数组中
- 此时callbacks数组为callbacks = [flushSchedulerQueue,nextTickCb]
- 微任务队列不变,仍然是microTasks = [flushCallbacks,promiseCb]
- 此时,同步代码执行完毕,开始执行微任务队列
- 结合microTasks和callbacks,此时的完整的函数执行的队列,应该是这样的,我干脆给它起个名字叫funcQueue好了,funcQueue = [[flushSchedulerQueue,nextTickCb],promiseCb]
- 所以,先执行flushSchedulerQueue,更新视图,
- 再执行nextTickCb,输出p1.innerHTML:3
- 再执行promiseCb,输出promise:3
总结
当然是要总结一点东西的
我们分析类似nextTick问题的时候,我们心中一定要明白几个关键的东西
- 任务队列queue,存放的是要处理的watcher
- 冲刷函数flushSchedulerQueue,会遍历queue去处理watcher更新视图
- callbacks数组,存放的是vue内部执行nextTick和用户执行this.$nextTick,传入的函数。
- vue内部执行nextTick传入的是冲刷函数flushSchedulerQueue
- 用户执行this.$nextTick,传入的是回调函数函数。
- callbacks冲刷函数flushCallbacks,遍历callbacks,处理里面的方法
- microTasks,微任务队列
搞清楚明白这些,再做类似的分析,慢慢分析,一定可以的。