Event Loop
(事件循环)是一个很重要的概念,指的是计算机系统的一种运行机制。 JavaScript
采用的就是这种机制,来解决单线程运行带来的一些问题。
JavaScript
是一种单线程语言,所有任务都是在一个线程上执行,如果一个任务等待时间长,会在成页面“假死”。如果采用多线程,会占用多倍的系统资源,也闲置多倍的资源。 Event Loop
就是为了解决这个问题提出的。
“Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)”
简单点说,就是在程序中设置两个线程:一个负责程序本身的运行,成为“主线程”;另一个负责主线程和其他进程(主要是各种I/O操作)的通信,被称为“ Event Loop
线程”。
Event Loop
本质上是一个 user agent
上协调处理各类事件的机制。各种浏览器的事件同时触发时,肯定有一个先来后到的排队问题。解决这些事件如何排队触发的机制,就是 Event Loop
。这个排队行为在开发人员来看,可以分为两个队列:
- 外部队列(宏任务/MacroTask):主要是浏览器协调的各类事件的队列,标准文件中称之为
Task Queue
- 内部队列(微任务/MicroTask):
JavaScript
内部执行的任务队列,标准文件中称之为MicroTask Queue
注:为了更好理解我们称之为队列,其本质是一个有序集合。因为传统的队列是 FIFO
(先进先出)的,而这里的队列到了前面如果为满足执行条件也是不会执行的。
外部队列
- DOM 操作(页面渲染)
- 用户交互(鼠标、键盘事件)
- 网络请求(
ajax
等) - History API 操作事件
- 定时器(
setTimeout/setInterval
等)
内部队列
- Promise 的
then
和catch
方法 - MutationObserver (监听
DOM
的更改) - Object.observe (已废弃,用于监听对象的修改,再调用回调,现在可以用
Proxy
代替)
注:除进入内部队列和外部队列的代码仍旧按照顺序执行。
setTimeout(() => {
console.log('setTimeout1')
})
Promise.resolve().then(() => {
console.log('promise1')
})
setTimeout(() => {
console.log('setTimeout2')
})
Promise.resolve().then(() => {
console.log('promise2')
})
Promise.resolve().then(() => {
console.log('promise3')
})
console.log('script end');
// script end - promise1 - promise2 - promise3 - setTimeout1 - setTimeout2
JavaScript 在浏览器端的执行顺序是:
- 一次外部事件
- 所有内部事件
- HTML 渲染
- 回到第一
注:一次 script
脚本的加载就是一个外部事件,所以 JavaScript
的解析仍然是按照这个顺序执行。
setImmediate
setTimeout
延迟的时间精度是毫秒( ms ),但实际上事件循环的时间可能是纳秒级的,所以一次事件循环的多个外部队列中,找到某一个队列直接执行其中的 callback
可以得到比 setTimeout
更早执行的效果。因此为了解决时间精度问题, JavaScript
引入了 setImmediate
。
注:setImmediate
只在 Node.js
中生效,在浏览器端不生效。
Node.js 中的 Event Loop
const fs = require('fs');
setImmediate(() => {
console.log('setImmediate1');
});
fs.readDir(__dirname, () => {
console.log('fs.readDir');
});
setTimeout(()=>{
console.log('setTimeout1');
}, 0);
Promise.resolve().then(() => {
console.log('promise');
});
setImmediate(() => {
console.log('setImmediate2');
});
setTimeout(()=>{
console.log('setTimeout2');
}, 200);
console.log("end");
// end - promise - setTimeout1 - fs.readDir - setImmediate1 - setImmediate2 - setTimeout2
在 Node.js
中, JavaScript
的执行顺序稍有变化,在 Node.js 10.x
以上的版本运行(因为早些版本,运行顺序和浏览器不一致,早期的 Node.js
允许一次执行多个外部事件,而浏览器端,一次只允许执行一个外部事件)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MKclqWwQ-1606293606743)(./img/node-queue-all.png)]
各阶段描述:
-
timers
处理
setTimeout
与setInterval
回调,开始处理的时机与poll
阶段有关联。 -
pending callbacks
执行某些系统操作的回调。
-
idle, prepare
内部使用,忽略。
-
poll:
poll
是一个核心阶段,等新I/O
事件的触发,以及执行I/O
相关回调。Node.js
中出现异步的绝大部分情况都是I/O
操作,它们的回调基本都在这个阶段被执行。poll
阶段主要做两件事:- 计算需要为新的的I/O事件等待多久
当进入poll阶段,如果队列为空且不存在setImmediate与就绪的timer,Node.js会在这里block一定的时间等待新的I/O事件到来,然后立即执行其回调。这种情况具体block等待多久是不具体的,但如果在block一定时间后仍没有新到达的I/O事件,可以肯定循环依旧会进入check阶段或者回到timer阶段。
- 处理该阶段队列中的事件
当进入poll阶段,如果队列不为空且没有就绪的timer,Node.js会在这里执行队列中的callback直到队列为空或者执行的callback数达到系统设定的某个值。随后Node.js检查是否存在预设的setImmediate,存在话就进入check阶段,否则开始检查timer就绪情况选择回到timer阶段或者进入check阶段。
- 计算需要为新的的I/O事件等待多久
-
check
该阶段的执行内容是所有
setImmediate
回调。 -
close callbacks
socket的异常关闭,
close
事件的回调会在该阶段执行。
这篇文章是在理解 Event Loop
的执行顺序过程中总结而来,但在 Node
端的一个事件循环理解还不是十分透彻,也有去阅读一些其他文献,但是笔者目前在 Node
端具体代码应用还不太多,无法更深入地进行总结,如果有比较好的文章,欢迎推荐,一起学习!