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的时候塞入微任务队列。在一次宏任务完成后,会检查微任务队列有没有需要执行的任务,有的话按顺序执行微任务队列中所有的任务。之后再开始执行下一次宏任务。具体步骤:
- 执行主代码块
- 若遇到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断开。