javascript事件循环

前言

最近看到一道非常经典的面试题,感觉非常有趣:

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

new Promise((resolve) => {
    console.log(2);
    resolve();
}).then(() => {
    console.log(3);
});

console.log(4);
// 输出最后的结果

正确答案是2 4 3 1,不过这位面试官大佬也不是很专业,只知道答案并不知道原理,决定回家之后研究一番。

浏览器环境

单线程与异步

众所周知,JavaScript是一门单线程的执行语言,处理任务的时候是一件一件的往下处理。

console.log(1);
console.log(2);
console.log(3);
// 输出结果123

但大家在往常开发过程中常用的ajax、setTimeout之类的操作并没有阻塞进程:

console.log(1);
setTimeout(() => {
    console.log(2);
}, 0);
console.log(3);
// 输出结果1 3 2,setTimeout并没有阻塞后面的流程

其实JavaScript单线程是指浏览器在解释和执行javascript代码时只有一个线程,即JS引擎线程,浏览器自身还会提供其他线程来支持这些异步方法,浏览器的渲染线程大概有一下几种:

  • JS引擎线程
  • 事件触发线程
  • 定时触发器线程
  • 异步http请求线程
  • GUI渲染线程
  • ...

浏览器事件机制

浏览器在执行js代码过程中会维护一个执行栈,每个方法都会进栈执行之后然后出栈(FIFO)。与此同时,浏览器又维护了一个消息队列,所有的异步方法,在执行结束后都会将回调方法塞入消息队列中,当所有执行栈中的任务全部执行完毕后,浏览器开始往消息队列寻找任务,先进入消息队列的任务先执行。

 例如之前的代码的执行步骤:

console.log(1);
setTimeout(() => {
    console.log(2);
}, 0);
console.log(3);
  • 1.将console.log(1)丢进执行栈,并执行,执行完毕出栈。
  • 2.将setTimeout丢给浏览器异步进程执行。
  • 3.console.log(3)丢到执行栈,执行,执行完毕出栈。
  • 4.setTimeout时间到,把回调丢给消息队列。
  • 5.此时执行栈为空,从消息队列取任务执行,console.log(2)执行。

宏任务和微任务

 

那么如果两个不同种类的异步任务执行后,哪个会先执行?就像开头提到的面试题,setTimeout和promise哪个会先执行?这时候要提到概念:宏任务微任务。 概念如下:

  • 宏任务:js同步执行的代码块,setTimeout、setInterval、XMLHttprequest等。
  • 微任务:promise、process.nextTick(node环境)等。

执行栈中执行的任务都是宏任务,当宏任务遇到Promise的时候会创建微任务,当Promise状态fullfill的时候塞入微任务队列。在一次宏任务完成后,会检查微任务队列有没有需要执行的任务,有的话按顺序执行微任务队列中所有的任务。之后再开始执行下一次宏任务。具体步骤:

  1. 执行主代码块
  2. 若遇到Promise,把then之后的内容放进微任务队列
  3. 一次宏任务执行完成,检查微任务队列有无任务
  4. 有的话执行所有微任务
  5. 执行完毕后,开始下一次宏任务。
setTimeout(() => {
    console.log(1);
}, 0);

new Promise((resolve) => {
    console.log(2);
    resolve();
}).then(() => {
    console.log(3);
});

console.log(4);

这道面试题的步骤:

  1. setTimeout丢给浏览器的异步线程处理,因为时间是0,马上放入消息队列
  2. new Promise里面的console.log(2)加入执行栈,并执行,然后退出
  3. 直接resolve,then后面的内容加入微任务队列
  4. console.log(4)加入执行栈,执行完成后退出
  5. 检查微任务队列,发现有任务,执行console.log(3)
  6. 发现消息队列有任务,执行下一次宏任务console.log(1)

node环境

node环境中的事件机制要比浏览器复杂很多,node的事件轮询有阶段的概念。每个阶段切换的时候执行,process.nextTick之类的所有微任务。

 

 

timer阶段

执行所有的时间已经到达的计时事件

peding callbacks阶段

这个阶段将执行所有上一次poll阶段没有执行的I/O操作callback,一般是报错。

idle.prepare

可以忽略

poll阶段

这个阶段特别复杂

  1. 阻塞等到所有I/O操作,执行所有的callback.
  2. 所有I/O回调执行完,检查是否有到时的timer,有的话回到timer阶段
  3. 没有timer的话,进入check阶段.

check阶段

执行setImmediate

close callbacks阶段

执行所有close回调事件,例如socket断开。

posted @ 2020-09-08 01:01  Magi黄元  阅读(136)  评论(0编辑  收藏  举报