前言
最近看到一道非常经典的面试题,感觉非常有趣:
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的时候塞入微任务队列。在一次宏任务完成后,会检查微任务队列有没有需要执行的任务,有的话按顺序执行微任务队列中所有的任务。之后再开始执行下一次宏任务。具体步骤:
- 执行主代码块
- 若遇到Promise,把then之后的内容放进微任务队列
- 一次宏任务执行完成,检查微任务队列有无任务
- 有的话执行所有微任务
- 执行完毕后,开始下一次宏任务。
setTimeout(() => {
console.log(1);
}, 0);
new Promise((resolve) => {
console.log(2);
resolve();
}).then(() => {
console.log(3);
});
console.log(4);
这道面试题的步骤:
- setTimeout丢给浏览器的异步线程处理,因为时间是0,马上放入消息队列
- new Promise里面的console.log(2)加入执行栈,并执行,然后退出
- 直接resolve,then后面的内容加入微任务队列
- console.log(4)加入执行栈,执行完成后退出
- 检查微任务队列,发现有任务,执行console.log(3)
- 发现消息队列有任务,执行下一次宏任务console.log(1)
node环境
node环境中的事件机制要比浏览器复杂很多,node的事件轮询有阶段的概念。每个阶段切换的时候执行,process.nextTick之类的所有微任务。
timer阶段
执行所有的时间已经到达的计时事件
peding callbacks阶段
这个阶段将执行所有上一次poll阶段没有执行的I/O操作callback,一般是报错。
idle.prepare
可以忽略
poll阶段
这个阶段特别复杂
- 阻塞等到所有I/O操作,执行所有的callback.
- 所有I/O回调执行完,检查是否有到时的timer,有的话回到timer阶段
- 没有timer的话,进入check阶段.
check阶段
执行setImmediate
close callbacks阶段
执行所有close回调事件,例如socket断开。
招聘!!!
字节跳动互娱基础架构团队招人啦!北京、深圳、杭州都有岗位!
我们是谁
字节成立最早的前端架构团队,目前规模最大,做的最专业,手里直接有大几百人的前端业务团队,产品 DAU 上亿级别,每天不用和 PM、UI 撕逼,有良好的技术氛围,业界大牛云集,团队成员都能获得相对好的技术成长。
平时工作
负责抖音、抖音火山版、直播等业务大规模复杂业务场景的前端架构设计、实现和优化
- 负责PC、H5、Hybrid、App Native、BFF、RPC等一种或几种技术场景的架构;
- 制定开发规范,工程化体系搭建及优化,提升开发效率、质量和性能,保障业务稳定运行;
- 发现现有流程及架构的问题,并持续进行优化;
- 解决业务遇到的技术痛点和难点;
- 跟进业内前沿技术,保证团队技术的先进性。
职位要求
- 本科及以上学历,计算机及相关专业;计算机基础扎实,熟悉数据结构、网络等;
- 有一定的架构和方案设计能力及经验,具备一定的方案沟通和推动能力;
- 对后端技术有一定了解,熟悉一门后端语言(java/go等);
- 对前端工程化(例如构建方面:webpack、rollup等)、Nodejs、渲染框架(例如react或vue等)、中后台搭建系统等至少其一有一定深度的实践经验者优先;
- 有大型网站架构经验者优先;有较高的技术热情和积极性者优先。
加分项
- 参与或主导过优秀的开源项目;
- 有优秀的技术博文、博客。
有意者可以添加我微信说明来意: