执行顺序:同步代码 > nextTick > Promise> setTimeout
结论先行,如果你只需要答案,看到这一步即可
如果你想进一步了解为何会是这样的执行顺序,请你预留出20分钟的时间,我会带着你由浅入深来了解其背后的运行原理,其中涉及到的知识点有:异步
、宏任务和微任务
、Event Loop事件循环
、JavaScript调用栈
和任务队列
,不了解这些知识点也没关系,我会用生活中最通俗易懂的例子来让你理解;观点会加上自己的理解,如果理解有误还请评论区指出,Best!
1)同步
由于 JavaScript 运行时,会阻塞浏览器解析引擎的主线程(如果不理解,可以看这篇文章了解一下浏览器的渲染机制),因此同步代码可以理解为:立即执行且必须执行
。单位时间内,只会做一件事。如果中途出现错误,就会导致主线程阻塞,从而导致页面报错。
同步代码示例如下:
function sync() {
console.log('我是同步执行的代码')
}
sync()
注意:
同步代码会先于所有其他代码执行!哪怕用 async/await
,也会先执行同步代码
2)异步
由同步的原理可知,有些事情并非是当下立即需要执行的,因此就会使用异步,告知浏览器,我过段时间再执行这个函数。从网上看到过一个关于异步的理解很棒的例子,分享给你:
异步就是有一件不需马上做的事,你先给他人打了声招呼,告诉他满足某个条件的时候,就干这件事。
就如你睡觉前定了第二天6点的闹钟,这时候你就给了手机一个异步的事件,当时间到6点的时候,触发了这个条件,闹钟响起。而好处就是,你设置好这个异步的条件之后,就可以去干别的事,完全不会影响主线程上事件的执行。
异步代码示例如下:
setTimeout(() => {
console.log('我在1秒后才执行')
}, 1000)
new Promise().then(() => {
console.log('我是异步执行的回调函数')
})
3)同步和异步代码执行顺序
console.log('1')
setTimeout(() => {
console.log('5')
}, 1000)
new Promise((resolve) => {
console.log('2');
resolve()
}).then(() => {
console.log('4');
});
console.log('3')
// 最终打印结果
1
2
3
4
5
对于上面的打印结果,解析如下:
1)执行顺序:微任务 > 宏任务
常见的宏任务有:
script
setTimeout / setInterval
常见的微任务有:
Promise
process.nextTick
有一个已知的执行顺序是:
微任务会先于宏任务执行。
因此,在上述的代码示例中,Promise.then
中的回调函数先于 setTimeout
中的回调函数执行。
2)微任务内部的顺序之争
不讨论宏任务内部的顺序原因在于,我们常用的用法基本都是延迟x秒去执行回调,而执行顺序也就取决于这个延迟。
回到文章的标题:Promise
、nextTick
的执行顺序,同为微任务,它们谁先谁后?
注:
nextTick 需在 Node.js 环境下才可使用,常用场景为在 Vue 框架中使用
new Promise((resolve) => {
console.log(1);
process.nextTick(() => {
console.log(2);
});
resolve();
process.nextTick(() => {
console.log(3);
});
console.log(4);
}).then(() => {
console.log(5);
});
setTimeout(() => {
console.log(6);
}, 0);
console.log(7);
// 输出顺序
1
4
7
2
3
5
6
结论显而易见,nextTick
和 Promise.then
的执行顺序为:nextTick > Promise.then
为何 nextTick 会比 Promise 快呢?
微任务
和宏任务
而言,内部的回调函数都需要进队列
中进行排队;而 nextTick
是直接告诉引擎,在 nextTick
中的回调需要尽快执行
,而非放入队列。(参考图一)nextTick
虽为微任务,但会在一个事件循环结束之后
,下一个事件循环开始之前
进行回调的调用(关于事件循环,后面会讲到)nextTick
其实也是使用 Promise
来处理回调,但是会将 nextTick 中的回调全部塞到 Promise.then 的回调之前
,而同级微任务的情况下,是按顺序来进行执行的,这就让 nextTick 永远都在 Promise.then 之前执行。(参考图二)1)调用栈 & 任务队列
如下图所示
排序
(排队)调用栈
这个过程就像你去银行办理业务,由于业务员只有一位,所以大家需要排队等待办理
等到轮到你的时候,你便来到业务窗口前办理业务,这便是入栈
等到办理完后,你离开业务柜台,这便是出栈
假如你在办理期间有个人的事务更加紧急,于是他优先办理后,他优先离开了,那你就需要等他办理后,业务员才能继续办理你的业务,这便是先入后出的特性。
2)宏任务队列 & 微任务队列
第3大点中,我们提到宏任务和微任务,它们其实是两个队列,且宏任务在微任务之后,如下图所示
1)事件循环是什么?
当同步代码执行完后,就会开启 Event Loop
,不断去循环回调队列,看队列中是否有回调函数需要被执行
若队列中有回调函数需要执行,且当前调用栈为空,则将该回调函数推入到调用栈并执行
若调用过程中发现回调函数中有内嵌的回调函数,则继续将回调函数丢入回调队列中,等当前的回调调用完成后,再开启新一轮的回调函数调用
举个例子:
当朋友和你说,给你寄了一张明信片,但是不知道什么时候会到(众所周知,邮局送过来的信和明信片都不会有电话或短信通知)。这一步相当于朋友给你设置了一个异步事件,你不知道什么时候会触发。因此,你每天都会去门口的信箱看一眼,看里面是否有寄过来的明信片,如果有,你便可以取出。这一步相当于你开启了 Event Loop,每天都去回调队列中看是否有回调函数被放入,如果有,便取出并在调用栈中运行。
2)是如何工作的?
Event Loop
,其中分为微任务
队列和宏任务
队列微任务队列
中是否有回调函数,且调用栈是否为空,若是则把该函数从队列中推入到调用栈执行宏任务队列
中是否有回调函数,且调用栈是否为空,若是则把该函数从队列中推入到调用栈执行开启下一个循环
,从第2步开始一直反复3)回调函数中嵌套子回调函数怎么办?
如果把一个循环称为 tick
那么下面代码中,打印出1的部分,则会在第一个 tick
周期中调用
而打印出2的部分,会被放入第二个 tick
周期中进行调用,以此类推
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
}, 0);
}, 0);
我们再一次通过 Event Loop
的视角,来回看标题上的问题:
Promise
、nextTick
、setTimeout
的执行顺序
同步代码
事件循环
nextTick 中的回调
微任务
队列中的回调宏任务
队列中的回调结束
一个循环因此,Promise > nextTick > setTimeout