- 完整的事件循环机制需要了解两种异步队列:
macro-task
和micro-task
macro-task
常见的有setTimeout, setInterval, setImmediate, script脚本, I/O操作,UI渲染
micro-task
常见的有promise, process.nextTick, MutationObserver
等- 完整事件循环流程为:4.1
micro-task
空,macro-task
队列只有script
脚本,推出macro-task
的script
任务执行,脚本执行期间产生的macro-task,micro-task
推到对应的队列中4.2 执行全部micro-task
里的微任务事件4.3 执行DOM
操作,渲染更新页面4.4 执行web worker
等相关任务4.5 循环,取出macro-task
中一个宏任务事件执行,重复4的操作。
从上面的流程中我们可以发现,最好的渲染过程发生在微任务队列的执行过程中,此时他离页面渲染过程最近,因此我们可以借助微任务队列来实现异步更新,它可以让复杂批量的运算操作运行在JS层面,而视图的渲染只关心最终的结果,这大大降低了性能的损耗。
举一个这一做法好处的例子: 由于Vue
是数据驱动视图更新渲染,如果我们在一个操作中重复对一个响应式数据进行计算,例如 在一个循环中执行this.num ++
一千次,由于响应式系统的存在,数据变化触发setter
,setter
触发依赖派发更新,更新调用run
进行视图的重新渲染。这一次循环,视图渲染要执行一千次,很明显这是很浪费性能的,我们只需要关注最后第一千次在界面上更新的结果而已。所以利用异步更新显得格外重要。
Vue
用一个queue
收集依赖的执行,在下次微任务执行的时候统一执行queue
中Watcher
的run
操作,与此同时,相同id
的watcher
不会重复添加到queue
中,因此也不会重复执行多次的视图渲染。我们看nextTick
的实现。
1.如果浏览器执行Promise
,那么默认以Promsie
将执行过程推到微任务队列中。
var timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
// 手机端的兼容代码
if (isIOS) { setTimeout(noop); }
};
// 使用微任务队列的标志
isUsingMicroTask = true;
flushCallbacks
是异步更新的函数,他会取出callbacks数组的每一个任务,执行任务,具体定义如下:
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
// 取出callbacks数组的每一个任务,执行任务
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
2.不支持promise
,支持
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = function () {
setImmediate(flushCallbacks);
};
}
4.所有方法都不适合,会使用宏任务方法中的setTimeout
else {
setTimeout(flushCallbacks, 0);
};
}
当nextTick
不传递任何参数时,可以作为一个promise
用,例如:
说了这么多原理性的东西,回过头来看看nextTick
的使用场景,由于异步更新的原理,我们在某一时间改变的数据并不会触发视图的更新,而是需要等下一个tick
到来时才会更新视图,下面是一个典型场景:
<input v-if="show" type="text" ref="myInput">
// js
data() {
show: false
},
mounted() {
this.show = true;
this.$refs.myInput.focus();// 报错
}
mounted() {
this.show = true;
this.$nextTick(function() {
this.$refs.myInput.focus();// 正常