面试官:只用这一道题我就能知道你对EventLoop了解多少?

各位读者可以先思考一下这道题的输出结果是什么?我第一次答这题是错了。。。

console.log('start');

Promise.resolve()
  .then(() => {
    
    
  console.log('p1');
}).then(() => {
    
    
  console.log('p2');
});
(new Promise(resolve => {
    
    
    console.log('p3')
    resolve()
})).then(() => {
    
    
    console.log('p4')
});

setTimeout(() => {
    
    
  Promise.resolve()
    .then(() => {
    
    
    console.log('p5');
  }).then(() => {
    
    
    console.log('p6');
  });
  setInterval(() => {
    
    
    console.log('interval');
  },3000);
  setTimeout(() => {
    
    
    console.log('timeout1');
  }, 0);
  console.log('timeout2');
},0);
console.log('end')

EventLoop核心知识点

网上讲解EventLoop的文章数不胜数,可以阅读阮一峰老师的《再谈Event Loop》和creeperyang的《从Promise来看JavaScript中的Event Loop、Tasks和Microtasks》

进程与线程

进程:浏览器是多进程的,每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等
线程:渲染进程又包括GUI 渲染线程(负责渲染页面,解析 HTML,CSS 构成 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程

宏任务与微任务

宏任务:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
微任务:Promise、MutaionObserver、process.nextTick(Node.js 环境)

循环顺序

在这里插入图片描述

事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。

浏览器单个Tick核心步骤

  1. 在此次 tick 中检查宏任务队列中的队头任务( oldest task,最先进入队列的任务 ),如果有则推入执行栈执行(一次)
  2. 检查微任务队列是否存在任务,如果存在则不停地推入执行栈执行,直至清空微任务队列
  3. 进入更新渲染阶段,判断是否需要渲染。这里有一个 rendering opportunity 的概念,也就是说不一定每一轮 event loop 都会对应一次浏览 器渲染,要根据屏幕刷新率、页面性能、页面是否在后台运行来共同决定,通常来说这个渲染间隔是固定的。(所以多个 task 很可能在一次渲染之间执行)
  4. 重复执行上述步骤

牢记一点

JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。

Promise核心知识点

推荐阅读《Promise的源码实现(完美符合Promise/A+规范)》

  1. 根据Promises/A+规范,then方法的两个回调函数需要放入异步任务中,如果模拟实现通常使用setTimeout放入宏任务中,但是平台实现则是放入微任务中。

2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

  1. 只有当前promise处于非pending状态(即处于fulfilled或者rejected状态)时,才立即放入微任务队列中,否则等待Promise状态改变才推入微任务队列。

2.3.2 If x is a promise, adopt its stat:
2.3.2.1 If x is pending, promise must remain pending until x is fulfilled or rejected.
2.3.2.2 If/when x is fulfilled, fulfill promise with the same value.
2.3.2.3 If/when x is rejected, reject promise with the same reason.

  1. Promise构造函数执行器(executor)中的同步代码立即执行。

图解答案

脚本执行过程中关键点的执行上下文栈、宏任务队列、微任务队列情况。

扫描二维码关注公众号,回复: 12897341 查看本文章
  1. 脚本开始执行
    在这里插入图片描述

  2. 执行console.log('start'),输出’start’。弹出执行栈
    在这里插入图片描述

  3. 执行Promise.resolve().then(…)方法,因为promise已经是fulfilled状态,所以回调函数() => { console.log(‘p1’) }直接推入微任务队列中。此时第一个then回调函数并未为执行,对于第二个then方法而言,promise还处于pending状态,所以等待promise状态改变。
    在这里插入图片描述

  4. Promise构造函数中的同步代码立即执行
    在这里插入图片描述

  5. 执行console.log('p3'),输出’p3’,promise变为fulfilled状态,then方法的回调函数放入微任务队列中
    在这里插入图片描述

  6. 执行setTimeout定时器,放入定时器线程中执行,因为设置为0ms(如果定时器嵌套五层,最少4ms后才会执行,具体可阅读《为什么 setTimeout 有最小时延 4ms ?》),所以定时器线程0ms后就会将任务推入宏任务队列中。
    在这里插入图片描述
    在这里插入图片描述

  7. 执行console.log('end'),输出’end’
    在这里插入图片描述

  8. script脚本也属于宏任务,故当前tick下依次执行并清空微任务队列,首先执行队首的任务。输出’p1’,执行完成后Promise变成fulfilled状态,所以将第二个then()方法的回调函数放入微任务队列中。
    在这里插入图片描述

  9. 继续执行微任务队列中任务,依次输出’p4’, ‘p2’
    在这里插入图片描述
    在这里插入图片描述

  10. 微任务清空完成,取出宏任务队列中队首任务放入执行栈中执行。
    在这里插入图片描述

  11. 与上面第三步相似,第一个then方法的回调函数先推入微任务队列中
    在这里插入图片描述

  12. 执行setInterval定时器,等待定时器每3秒将回调任务加入宏任务队列。
    在这里插入图片描述

  13. 执行setTimeout定时器,回调函数在0ms后推入宏任务队列。
    在这里插入图片描述

  14. 执行console.log('timeout2'),输出’timeout2’。
    在这里插入图片描述

  15. 依次执行并清空微任务队列,与第8步同理。输出’p5’
    在这里插入图片描述

  16. 继续执行微任务队列中任务,输出’p6’
    在这里插入图片描述

  17. 微任务清空完成,取出宏任务队列中队首任务放入执行栈中执行,输出’timeout1’
    在这里插入图片描述

  18. 约3秒,宏任务队列推入interval定时器回调函数,等待下一tick执行
    在这里插入图片描述
    在这里插入图片描述
    动图效果如下:
    在这里插入图片描述

所以最终输出如下:

start
p3
end
p1
p4
p2
timeout2
p5
p6
timeout1
interval
interval

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/sinat_36521655/article/details/111182639