【JavaScript】用了这么久的Vue,你还不知道Promise、nextTick、setTimeout的执行顺序?(涉及EventLoop,由浅入深)

1. 结论

  执行顺序:同步代码 > nextTick > Promise> setTimeout


  结论先行,如果你只需要答案,看到这一步即可

  如果你想进一步了解为何会是这样的执行顺序,请你预留出20分钟的时间,我会带着你由浅入深来了解其背后的运行原理,其中涉及到的知识点有:异步宏任务和微任务Event Loop事件循环JavaScript调用栈任务队列,不了解这些知识点也没关系,我会用生活中最通俗易懂的例子来让你理解;观点会加上自己的理解,如果理解有误还请评论区指出,Best!

2. 同步 & 异步

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/2/3 均为同步代码,特别注意 2 的地方,该区域并非回调函数,而是同步代码!
  • 4/5 均为异步代码。但是同为异步,为何 4 的 Promise.then 会比 setTimeout 中的回调来得更早呢?那便涉及到宏任务和微任务的问题,继续往下看!

3. 宏任务 & 微任务

1)执行顺序:微任务 > 宏任务

常见的宏任务有:

  1. script
  2. setTimeout / setInterval
  3. `setImmediate``

常见的微任务有:

  1. Promise
  2. process.nextTick

有一个已知的执行顺序是:

微任务会先于宏任务执行。

因此,在上述的代码示例中,Promise.then 中的回调函数先于 setTimeout 中的回调函数执行。

2)微任务内部的顺序之争

不讨论宏任务内部的顺序原因在于,我们常用的用法基本都是延迟x秒去执行回调,而执行顺序也就取决于这个延迟。

回到文章的标题:PromisenextTick 的执行顺序,同为微任务,它们谁先谁后?

注: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

  结论显而易见,nextTickPromise.then 的执行顺序为:nextTick > Promise.then

为何 nextTick 会比 Promise 快呢?

  • 对于微任务宏任务而言,内部的回调函数都需要进队列中进行排队;而 nextTick 是直接告诉引擎,在 nextTick 中的回调需要尽快执行,而非放入队列。(参考图一)
  • nextTick 虽为微任务,但会在一个事件循环结束之后下一个事件循环开始之前进行回调的调用(关于事件循环,后面会讲到)
  • 从 nextTick 的源码来看,大致原理可以理解为:nextTick 其实也是使用 Promise 来处理回调,但是会将 nextTick 中的回调全部塞到 Promise.then 的回调之前,而同级微任务的情况下,是按顺序来进行执行的,这就让 nextTick 永远都在 Promise.then 之前执行。(参考图二)

【JavaScript】用了这么久的Vue,你还不知道Promise、nextTick、setTimeout的执行顺序?(涉及EventLoop,由浅入深)_第1张图片

【JavaScript】用了这么久的Vue,你还不知道Promise、nextTick、setTimeout的执行顺序?(涉及EventLoop,由浅入深)_第2张图片

4. 调用栈 & 任务队列

1)调用栈 & 任务队列

如下图所示

  • 当有函数需要执行而分配给浏览器引擎时,引擎会按一定顺序将函数进行排序(排队)
  • 排到该函数后,该函数推入到 JavaScript 主线程中的调用栈
  • 该函数执行完毕后,出栈

这个过程就像你去银行办理业务,由于业务员只有一位,所以大家需要排队等待办理


等到轮到你的时候,你便来到业务窗口前办理业务,这便是入栈


等到办理完后,你离开业务柜台,这便是出栈


假如你在办理期间有个人的事务更加紧急,于是他优先办理后,他优先离开了,那你就需要等他办理后,业务员才能继续办理你的业务,这便是先入后出的特性。

【JavaScript】用了这么久的Vue,你还不知道Promise、nextTick、setTimeout的执行顺序?(涉及EventLoop,由浅入深)_第3张图片

2)宏任务队列 & 微任务队列

  第3大点中,我们提到宏任务和微任务,它们其实是两个队列,且宏任务在微任务之后,如下图所示

【JavaScript】用了这么久的Vue,你还不知道Promise、nextTick、setTimeout的执行顺序?(涉及EventLoop,由浅入深)_第4张图片

5. Event Loop 事件循环

1)事件循环是什么?

  当同步代码执行完后,就会开启 Event Loop,不断去循环回调队列,看队列中是否有回调函数需要被执行

  若队列中有回调函数需要执行,且当前调用栈为空,则将该回调函数推入到调用栈并执行
  若调用过程中发现回调函数中有内嵌的回调函数,则继续将回调函数丢入回调队列中,等当前的回调调用完成后,再开启新一轮的回调函数调用

举个例子:
  当朋友和你说,给你寄了一张明信片,但是不知道什么时候会到(众所周知,邮局送过来的信和明信片都不会有电话或短信通知)。这一步相当于朋友给你设置了一个异步事件,你不知道什么时候会触发。

  因此,你每天都会去门口的信箱看一眼,看里面是否有寄过来的明信片,如果有,你便可以取出。这一步相当于你开启了 Event Loop,每天都去回调队列中看是否有回调函数被放入,如果有,便取出并在调用栈中运行。

2)是如何工作的?

  1. 同步代码执行完后,开启 Event Loop,其中分为微任务队列和宏任务队列
  2. 检查微任务队列中是否有回调函数,且调用栈是否为空,若是则把该函数从队列中推入到调用栈执行
  3. 检查宏任务队列中是否有回调函数,且调用栈是否为空,若是则把该函数从队列中推入到调用栈执行
  4. 直到微任务和宏任务队列中均为空,则开启下一个循环,从第2步开始一直反复

3)回调函数中嵌套子回调函数怎么办?

  如果把一个循环称为 tick

  那么下面代码中,打印出1的部分,则会在第一个 tick 周期中调用

  而打印出2的部分,会被放入第二个 tick周期中进行调用,以此类推

setTimeout(() => {
     
  console.log(1)
    
  setTimeout(() => {
     
    console.log(2)
  }, 0);
}, 0);

6. 总结

我们再一次通过 Event Loop 的视角,来回看标题上的问题:

PromisenextTicksetTimeout的执行顺序

  1. 首先执行所有同步代码
  2. 开启第一个 tick 周期的事件循环
  3. 在每个循环开始之前,执行 nextTick 中的回调
  4. 执行微任务队列中的回调
  5. 执行宏任务队列中的回调
  6. 结束一个循环
  7. 在一个周期结束之后,新周期开始之前,执行第3步的 nextTick,继续循环

因此,Promise > nextTick > setTimeout

7. 扩展

  • 执行顺序:微任务 > DOM渲染 > 宏任务

你可能感兴趣的:(面试,node.js,前端,队列,javascript,node.js,js,面试)