基本概念
事件循环机制(Event Loop) 本身是一个排序机制。
其中我们常见的事件循环本身其实是在 HTML 标准文档中定义的:
html.spec.whatwg.org/multipage/w…
根据标准中对定义的描述总结:事件循环本质上是浏览器用于协调用户交互、脚本执行、渲染、网络等事件的一个机制。
所以,”JS的事件循环“这个描述不太严谨。因为无论是从规范的定义还是从实现来讲,都不能说事件循环是”JS的“。
浏览器的事件循环
事件循环流程
在浏览器中基本按照HTML的规范来实现:
根据规范,每个线程都有一个事件循环(Event Loop)。 每个事件循环有至少一个任务队列(Task Queue,也可以称作Macrotask宏任务),各个任务队列中放置着不同来源(或者不同分类)的任务,可以让浏览器根据自己的实现来进行优先级排序。
以及一个微任务队列(Microtask Queue),主要用于处理一些状态的改变,UI渲染工作之前的一些必要操作(可以防止多次无意义的UI渲染)。
详细的执行步骤如下:
- 从任务队列中取出一个宏任务并执行
- 检查微任务队列,执行并清空微任务队列,如果在微任务的执行中又加入了新的微任务,也会在这一步一起执行
- 微任务执行完毕后继续执行下一个宏任务
- 主线程不断重复上面的步骤
宏任务、微任务
宏任务队列
- DOM 操作响应
- 用户交互 (鼠标、键盘)
- 网络请求
- History API 操作
- 定时器 (setTimeout 等)
微任务队列(js语言内部的事件队列,没有明确的规定,通常有以下几种)
- Promise 的成功 .then 与失败 .catch (以及其他事件)
- MutationObserver
- Object.observe (已废弃)
代码题
console.log(1);
new Promise(resolve => {
resolve();
console.log(2);
})
.then(() => {
console.log(3);
});
setTimeout(() => {
console.log(4);
}, 0);
console.log(5);
// 结果 1、 2、 5、 3、 4
这个题结合我们上面的描述就能很轻易的做出来,这里唯一的坑就是 new Promise
时候马上执行,.then
里面的内容才是微任务队列中的内容。
Node的事件循环
前面我们说过,事件循环是引擎根据规范去实现的。而Node本身不涉及HTML所以不会遵循HTML的规范,它有一套自己的排队机制。
Node.js也使用V8作为Js引擎,但I/O处理方面使用了libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现的。
对于Node.js的事件循环,官网的描述:当Node.js启动时,它会初始化一个事件循环,来处理输入的脚本,这个脚本可能进行异步API的调用、调度计时器或调用process.nextTick(),然后开始处理事件循环。
Node.js 内嵌的Js引擎是V8,所以一般浏览器中包含的异步方式在 NodeJS 中也是一样的。除此之外,Node.js中还有一些其他的异步形式:
- 文件 I/O:异步加载本地文件。
setImmediate()
:与 setTimeout 设置 0ms 类似,在某些同步任务完成后立马执行。process.nextTick()
:在某些同步任务完成后立马执行。server.close、socket.on('close',...)
等:关闭回调。
这些异步任务的执行就需要依靠Node.js的事件循环机制了。
事件循环的流程
libuv引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。下面是Node官方文档介绍的Eventloop 事件循环的流程:
整个流程分为六个阶段,当这六个阶段执行完一次之后,才可以算得上执行了一次 Eventloop 的循环过程。下面来看下这六个阶段都做了哪些事:
- timers 阶段:执行timer(setTimeout、setInterval)的回调,由 poll 阶段控制;
- I/O callbacks 阶段:主要执行系统级别的回调函数,比如 TCP 连接失败的回调;
- idle, prepare 阶段:仅Node.js内部使用,可以忽略;
- poll 阶段:轮询等待新的链接和请求等事件,执行 I/O 回调等;
- check 阶段:执行 setImmediate() 的回调;
- close callbacks 阶段:执行关闭请求的回调函数,比如socket.on('close', ...)
注意:上面每个阶段都会去执行完当前阶段的任务队列,然后继续执行当前阶段的微任务队列,只有当前阶段所有微任务都执行完了,才会进入下个阶段,这里也是与浏览器中逻辑差异较大的地方。
其中,这里面比较重要的就是第四阶段:poll,这一阶段中,系统主要做两件事:
回到 timer 阶段执行回调 执行 I/O 回调
在进入该阶段时如果没有设定了 timer 的话,会出现以下情况: (1)如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制; (2)如果 poll 队列为空时,会出现以下情况:
如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调; 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去;
当设定了 timer 且 poll 队列为空,则会判断是否有 timer 超时,如果有的就会回到 timer 阶段执行回调。 这一过程的具体执行流程如下图所示:
Node中的宏任务和微任务
Node中事件循环的异步队列也分为两种:宏任务队列和微任务队列。
常见的宏任务:setTimeout
、setInterval
、setImmediate
、script(整体代码)、 I/O 操作
常见的微任务:process.nextTick
、new Promise().then(回调)
process.nextTick()
上面提到了process.nextTick()
,它是node中新引入的一个任务队列,它会在上述各个阶段结束时立即执行。
举个例子:
setTimeout(() => {
console.log('timeout')
}, 0);
Promise.resolve().then(() => {
console.error('promise')
})
process.nextTick(() => {
console.error('nextTick')
})
// nextTick 、promise 、timeout
可以看到,process.nextTick()
是优先于promise的回调执行。