开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
本文需要你了解 事件循环机制、微任务、宏任务。都是干货,希望你耐心看完。
案例代码
点击运行看结果
<template>
<div>
<div ref="count" >{{count}}</div>
<div>
<div>输出结果:</div>
<ol>
<li v-for="item in resList" :key="item">{{item}}</li>
</ol>
</div>
</div>
</template>
<script>
// import Vue 不是必须的,需要手动指定 Vue 的版本时,可以解开注释
// import Vue from 'vue';
export default {
data() {
return {
count: 0,
resList:[]
};
},
mounted(){
this.resList.push(`sync console1 innerHTML: ${this.$refs.count.innerHTML}`);
console.log(`sync console1 innerHTML: ${this.$refs.count.innerHTML}`)
this.count = 1;
this.count = 2;
this.resList.push(`sync console2 innerHTML: ${this.$refs.count.innerHTML}`);
console.log(`sync console2 innerHTML: ${this.$refs.count.innerHTML}`)
this.$nextTick(()=>{
this.resList.push(`nextTick1 innerHTML: ${this.$refs.count.innerHTML}`);
console.log(`nextTick1 innerHTML: ${this.$refs.count.innerHTML}`)
})
this.count = 3;
Promise.resolve().then(()=>{
this.resList.push(`Promise innerHTML: ${this.$refs.count.innerHTML}`);
console.log(`Promise innerHTML: ${this.$refs.count.innerHTML}`)
})
this.$nextTick(()=>{
this.resList.push(`nextTick2 innerHTML: ${this.$refs.count.innerHTML}`);
console.log(`nextTick2 innerHTML: ${this.$refs.count.innerHTML}`)
})
// this.count = 4;
this.resList.push(`sync console3 innerHTML: ${this.$refs.count.innerHTML}`);
console.log(`sync console3 innerHTML: ${this.$refs.count.innerHTML}`)
}
};
</script>
按照上面的测试代码看到如下输出
sync console1 innerHTML: 0
sync console2 innerHTML: 0
sync console3 innerHTML: 0
nextTick1 innerHTML: 3
nextTick2 innerHTML: 3
Promise innerHTML: 3
这时会有几个问题:
- 为什么sync console2输出的不是count值2?
- 为什么nextTick1输出的不是count值2?
- 为什么Promise输出在nextTick2后面?
从源码里找答案
我们都知道Vue2是用Object.defineProperty做响应式的,当我们执行 this.count = 1; 的时候,会触发set方法,所以我们就先从源码这里看。
set方法的最后执行了dep.notify(); 通知更新,但这时并不会马上更新页面,继续进入 notify() 内部看。
在这次测试代码中,this.subs是这个count对应的watcher,因为我们没有使用 $watch ,所以this.subs里只有一个watcher,并且它是负责更新这个组件的。
再进入到sub.update(); 内部看看。
如果是计算属性的watcher,会有lazy属性。$watch 会使用sync属性。我们只是单纯赋值,所以走queueWatcher(this) ,再进入内部看看。
has是一个对象,执行到这里会判断watcher是否重复,也就是说,我们测试代码中给count重复赋值,只有第一次count = 1; 时,组件的watcher会添加到 queue 中。
queue是负责存储 watcher 的一个数组。
flushing 是判断当前是否处于更新dom的过程中,如果是则插入到queue中,不是就直接push进去。
waiting 则表示在此次事件循环中,只执行一次。
flushSchedulerQueue 就是执行清空 queue 里的watcher,执行对应更新函数的方法,同时初始化 has = {};waiting = flushing = false;queue.length = 0;
nextTick 这个和我们使用的 Vue.$nextTick(),这里将 flushSchedulerQueue 作为回调函数传进去。
我们进入到 nextTick 内部看看。
callbacks 是一个数组。
timerFunc 是一个开启异步函数,它的实现是 Promise.resolve() 微任务 > MutationObserver 微任务 > setImmediate微任务 > setTimeout 宏任务
timerFunc方法会异步执行flushCallbacks方法,flushCallbacks方法会遍历执行 callbacks 里的回调函数,同时置空。
总结
- 第一个问题,因为每次赋值后更新页面是一个异步过程,所以 console2 输出的还是 0;
- 第二个问题,nextTick会将回调函数塞到callbacks中,而callbacks清空执行是异步的,所以实际上 this.count = 3;先执行。
- 第三个问题,因为当this.count = 1;时,此时的微任务队列中已经有了一个 timerFunc 方法负责,即使Promise 写在Vue.nextTick会直接将回调函数塞到 timerFunc 方法中的 callbacks 数组中,所以Promise在后面执行。
思考
如果将
Promise.resolve().then(()=>{
this.resList.push(`Promise innerHTML: ${this.$refs.count.innerHTML}`);
console.log(`Promise innerHTML: ${this.$refs.count.innerHTML}`)
})
写到 this.count = 1; 之前,它和nextTick1哪个先执行?