为什么setTimeout会比Promise后执行,明明写在Promise之前?
精读《前端面试之道》(四)--Event Loop - 图1

进程与线程

进程与线程的区别?js单线程带来什么好处?

js是单线程的。线程与进程都是对CPU工作时间片的一个描述

进程描述了CPU在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。线程是更小的单位,描述了执行一段指令所需的时间。

简而言之就是:进程>线程

例子:当打开一个浏览器的Tab页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、js引擎线程、http请求线程等等。

js运行的时候会阻止UI渲染。这说明js引擎线程和渲染线程是互斥的。因为js可以修改dom,UI线程也在工作。导致不安全的渲染。所以js是单线程的。

执行栈

什么是执行栈

可以把执行栈认为是一个储存函数调用的栈结构,遵循先进后出的原则

当开始执行代码的时候,首先会执行一个main函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈。
image.png
可以清晰地看到报错实在foo函数,foo函数优势在bar函数中调用的

当我们使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题。
image.png

浏览器中的event loop

异步代码执行顺序?什么是event loop?

我们已经知道当js代码执行的时候,其实是往执行函栈中放入函数,那么遇到异步代码呢?

其实当遇到异步代码的时候,会被挂起并在需要的时候加入到Task队列中。一旦执行栈为空,event loop就会从Task队列中拿出需要执行的代码并放入到执行栈中执行,所以本质上来说js中的异步还是同步行为。
image.png
不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task

  1. console.log('script start')
  2. async function async1() {
  3. await async2()
  4. console.log('async1 end')
  5. }
  6. async function async2() {
  7. console.log('async2 end')
  8. }
  9. async1()
  10. setTimeout(function() {
  11. console.log('setTimeout')
  12. }, 0)
  13. new Promise(resolve => {
  14. console.log('Promise')
  15. resolve()
  16. })
  17. .then(function() {
  18. console.log('promise1')
  19. })
  20. .then(function() {
  21. console.log('promise2')
  22. })
  23. console.log('script end')
  24. // script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

注意:新的浏览器中不是如上打印的,因为 await 变快了,具体内容可以往下看

首先解释async和await的执行顺序:

当我们调用async1函数时,会马上输出async2 end,并且函数返回一个Promise,接下来在遇到await的时候就会让出栈程序开始执行async1外的代码,所以我们完全可以把async看成是让出线程的标志。

然后当同步代码全部执行完毕以后,就会执行所有的异步代码,那么又会回到await的位置之行返回的Promise的resolve函数,这又会把resolve丢到微任务队列中,接下来去执行then中的回调,当两个then中的回调全部执行完毕以后,又会回到await的位置处理返回值,这时候你可以看成是Promise.resolve(返回值).then(),然后await后的代码全部被包裹进了then的回调中,所以cosnole.log(async1 end)会优先执行于setTimeout

如果你觉得上面这段解释还是有点绕,那么我把 async 的这两个函数改造成你一定能理解的代码

new Promise((resolve, reject) => {
  console.log('async2 end')
  // Promise.resolve() 将代码插入微任务队列尾部
  // resolve 再次插入微任务队列尾部
  resolve(Promise.resolve())
}).then(() => {
  console.log('async1 end')
})

也就是说,如果 await 后面跟着 Promise 的话,async1 end 需要等待三个 tick 才能执行到。那么其实这个性能相对来说还是略慢的,所以 V8 团队借鉴了 Node 8 中的一个 Bug,在引擎底层将三次 tick 减少到了二次 tick。但是这种做法其实是违法了规范的,当然规范也是可以更改的,这是 V8 团队的一个 PR,目前已被同意这种做法。

所以 Event Loop 执行顺序如下所示:

  • 首先执行同步代码,这属于宏任务
  • 当执行完所有同步代码以后,执行栈为空,查询是否有异步代码需要执行
  • 执行所有微任务
  • 当执行完所有微任务后,如有必要会渲染页面
  • 然后开始下一轮的Event Loop,执行宏任务中的异步代码,也就是setTimeout中的回调函数

所以以上代码虽然setTimeout写在Promise之前,但是因为Promise属于微任务而setTimeout属于宏任务,所以会有以上的打印。

微任务包括process.nextTick, promise, mutationObserver,其中process.nextTick为Node独有

宏任务包括script,setTimeout,setInterval, setImmediate, I/O, UI redering

误区:微任务不一定快于宏任务,因为宏任务包括了script,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。