javascript事件循环

2,574 阅读5分钟

前言

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

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

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

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

我当时写了2 4 1 3,最后面试官告诉我这是错误答案,正确答案是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断开。

招聘!!!

字节跳动互娱基础架构团队招人啦!北京、深圳、杭州都有岗位!

我们是谁

字节成立最早的前端架构团队,目前规模最大,做的最专业,手里直接有大几百人的前端业务团队,产品 DAU 上亿级别,每天不用和 PM、UI 撕逼,有良好的技术氛围,业界大牛云集,团队成员都能获得相对好的技术成长。

平时工作

负责抖音、抖音火山版、直播等业务大规模复杂业务场景的前端架构设计、实现和优化

  1. 负责PC、H5、Hybrid、App Native、BFF、RPC等一种或几种技术场景的架构;
  2. 制定开发规范,工程化体系搭建及优化,提升开发效率、质量和性能,保障业务稳定运行;
  3. 发现现有流程及架构的问题,并持续进行优化;
  4. 解决业务遇到的技术痛点和难点;
  5. 跟进业内前沿技术,保证团队技术的先进性。

职位要求

  1. 本科及以上学历,计算机及相关专业;计算机基础扎实,熟悉数据结构、网络等;
  2. 有一定的架构和方案设计能力及经验,具备一定的方案沟通和推动能力;
  3. 对后端技术有一定了解,熟悉一门后端语言(java/go等);
  4. 对前端工程化(例如构建方面:webpack、rollup等)、Nodejs、渲染框架(例如react或vue等)、中后台搭建系统等至少其一有一定深度的实践经验者优先;
  5. 有大型网站架构经验者优先;有较高的技术热情和积极性者优先。

加分项

  1. 参与或主导过优秀的开源项目;
  2. 有优秀的技术博文、博客。

有意者可以添加我微信说明来意:

image.png