1. 什么是 Event Loop?
"Event Loop是一个程序结构,用于等待和发送消息和事件。
(a programming construct that waits for and dispatches events or messages in a program.)"
举一个大家都熟知的栗子, 这样更能客观的理解。

大家都知道深夜食堂吧。厨师就一个人(服务端 Server)。最多再来一个服务生( 调度员 Event Loop )。晚上吃饭的客人(客户端 Client)很多。
1. 客人向服务生点完菜,就干自己事情,不用一直等着服务生, 服务生把一个人点的菜单送到厨师,
又去服务新的客人...
2. 厨师(服务端)只负责做客人们点的菜。
3. 服务生(调度员)不停的看厨师,一旦厨师做好菜了,按照标号送到相应的 客人(客户端)座位上

主线程
, 厨师线程 为 消息线程
。 客人每点一个菜。服务生就向厨师发出一个消息。并保留该消息的“标识”(回调函数
)用来接收厨师炒好的菜,并把菜送到相应的客人手中。
2. 同步模式
不管店里的客人多少,也不管每一份菜需要多久的时间做好。就只有厨师这一个人忙活。厨师一次只能服务一个客人。那这样的服务模式效率就比较低了。中途等待的时间比较长。
笔者认为 同步模式 就是没有 “服务生线程”, 厨师线程升级为 主线程
。
1. 第一个客人点了一份 "读取文件" , 炒好一份 "读取文件" 需要花费 1 分钟
2. 必须等第一个客人的菜炒好后,第二个客人才能点,并且点了一份 "读取数据库",
炒好一份 "读取数据库" 需要花费 2 分钟
3. 第三个客人点了一份 ...

从图中可以看出红色部份都是等待时间(或者是阻塞时间), 相当浪费资源。
假设我们现在只知道一种代码的执行方式 "同步执行", 也就是代码从上到下 按顺序执行。如果遇到 setTimeout , 也先这样理解。(实际上setTimeout 本身是立即执行的,只是回调函数异步执行)
console.log(1); //执行顺序1
setTimeout(function(){}, 1000); //执行顺序2
console.log(2); //执行顺序3
3. 异步模式
图表更能直观的反应这个概念:

主线程
不停的接收请求 request 和 响应请求 response, 真正处理任务的被 消息线程 event loop
安排其他相应的程序去执行,并接收相应的相应程序返回的消息。然后 reponse 给客户端。
1. 主线程干的事情非常简单,即 接收请求,响应请求, 因此可以能够处理更多的请求。而不用等待。
2. 消息线程维护请求,并把真正要做的事情交给对应的程序,并接收对应程序的回调消息,返回给 主线程
4. 几种调用模式的组合
- 同步阻塞
你跟你的女神表白,你女神立即回复你,而你也一直再等女神的回复
- 同步不阻塞
你跟你的女神表白, 你表白后,没有等女神来得及回复,你去忙你自己的事情了。你的女神立即回复了你
- 异步阻塞
你跟你的女神表白, 你女神没有立即回复你,说要考虑考虑,过几天答复你,而你也一直再等女神的回复
- 异步不阻塞
你跟你的女神表白,你表白后, 没有等女神的回复。你去忙你自己的事情了,女神也说她要考虑考虑,过几天再回复你
阻塞非阻塞 是指调用者
(表白的那个人)
同步异步 是指被调用者
(被表白的那个人)
同步异步取决于被调用者,阻塞非阻塞取决于调用者
5. 几个需要知晓的概念
-
宏任务 setTimeout , setInterval, setImmediate, I / O 操作
-
微任务 process.nextTick , 原生Promise (有些实现的Promise将then方法放到了宏任务中), Mutation Observer
console.log(1);
Promise.resolve('123').then(()=>{console.log('then')})
process.nextTick(function () {
console.log('nextTick')
})
console.log(2);

process.nextTick 优先于 promise.then 方法执行
6. 浏览器中的Event Loop
- 浏览器中js是单线程执行的。笔者称其为主线程, 主线程在运行过程中会产生 堆(heap)和 栈(stack), 所有同步任务都是在 栈中执行。
function one() {
let a = 1;
two();
function two() {
console.log(a);
let b = 2;
function three() {
//debugger;
console.log(b);
}
three();
}
}
one();
毫无疑问的是,上面这段代码执行的结果为:
1
2
在栈中都是以同步任务的方式存在:
再来看下面这段代码:
console.log(1);
setTimeout(function(){
console.log(2);
})
console.log(3);
执行结果为:
1
3
2
那到底是怎样执行的呢?

//宏任务
setTimeout(function(){
console.log(2);
})
//微任务
let p = new Promise((resolve, reject) => {
resolve(3);
});
p.then((data) => {
console.log(data);
}, (err)=>{
})
console.log(4);
执行结果为:
1
4
3
2
从这个可以看到。微任务消息队列的执行的
优先
于宏任务的消息队列.
console.log(1);
//宏任务
setTimeout(function(){
console.log(2);
})
//微任务
let p = new Promise((resolve, reject) => {
resolve(4);
});
p.then((data) => {
console.log(data);
}, (err)=>{
})
setTimeout(function(){
console.log(3);
})
console.log(5);
执行结果为:
1
5
4
2
3
每一次事件循环机制过程中,会将当前宏任务 或者 微任务消息队列中的任务都执行完成。然后再之前其他队列。
- 对于不能进入主线程执行的代码,笔者称其为异步任务, 这部分任务会进去消息队列(callback queue), 通过 事件循环机制 (event loop) 不停调用,进入 栈中进行执行。前提是栈中
当前
的所有任务(同步任务)都已经执行完成。

- 从图中,还可以得出这样的结论: 异步任务是通过 WebAPIs 的方式存入 消息队列。
- 上述过程总是在循环执行。
7. Node中的Event Loop
我们先来看看node是怎样运行的:

- js源码首先交给node 中的v8引擎进行编译
- 编译好的js代码通过node api 交给 libuv库 处理
- libuv库通过阻塞I/O和异步的方式,为每一个js任务(文件读取等等)创建一个单独的线程,形成多线程
- 通过Event Loop的方式异步的返回每一个任务执行的结果,然后返回给V8引擎,并反馈给用户
Event Loop 在整个Node 运行机制中占据着举足轻重的地位。是其核心。

每个阶段都有一个执行回调的FIFO队列。 虽然每个阶段都有其特定的方式,但通常情况下,当事件循环进入给定阶段时,它将执行特定于该阶段的任何操作, 然后在该阶段的队列中执行回调,直到队列耗尽或回调的最大数量 已执行。 当队列耗尽或达到回调限制时,事件循环将移至下一个阶段,依此类推。
timers:此阶段执行由setTimeout()和setInterval()调度的回调。
pending callbacks:执行I / O回调,推迟到下一个循环迭代。
idle,prepare:只在内部使用。
poll:检索新的I / O事件; 执行I / O相关的回调函数; 适当时节点将在此处阻塞。
check:setImmediate()回调在这里被调用。
close backbacks:一些关闭回调,例如 socket.on('close',...)。
timers阶段
需要注意的是:
const fs = require('fs');
function someAsyncOperation(callback) {
//假设需要95ms需要执行完成
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
//定义100ms后执行
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// 执行someAsyncOperation需要消耗95ms执行
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});
分析上述代码:
- someAsyncOperation方法时同步代码,先在栈中执行
- someAsyncOperation 中包含异步I/O, 需要花费95ms执行,加上 while的10ms, 因此需要105ms
- setTimeout 虽然定义的是在100ms后执行, 但由于 第一次轮询是到了 poll 阶段, 所以 setTimeout 需要等到第二轮事件轮询是执行。因此是在 105ms后执行
pending callbacks阶段
此阶段为某些系统操作(如TCP错误类型)执行回调。例如,
如果尝试连接时TCP套接字收到ECONNREFUSED,则某些* nix系统要等待报告错误。这将排队等候在待处理的回调阶段执行。
poll阶段
1.计算应该阻塞和轮询I / O的时间
2.处理轮询队列中的事件。
当事件循环进入poll阶段并且没有计时器时,会发生以下两件事之一:
1. 如果轮询队列不为空,则事件循环将遍历其回调队列,同步执行它们,直到队列耗尽或达到系统相关硬限制。
2. 如果轮询队列为空,则会发生以下两件事之一:
2.1 如果脚本已通过setImmediate()进行调度,则事件循环将结束轮询阶段并继续执行(check阶段)检查阶段以执行这些预定脚本。
2.2 如果脚本没有通过setImmediate()进行调度,则事件循环将等待将回调添加到队列中,然后立即执行它们。
一旦轮询队列为空,事件循环将检查已达到时间阈值的定时器。如果一个或多个定时器准备就绪,则事件循环将回退到定时器阶段以执行这些定时器的回调。
// poll的下一个阶段时check
// 有check阶段就会走到check中
let fs = require('fs');
fs.readFile('./1.txt',function () { //轮询队列已经执行完成,为空,即2.1中描述的
setTimeout(() => {
console.log('setTimeout')
}, 0);
setImmediate(() => {
console.log('setImmediate')
});
});
上面这段代码执行的过程阶段为:

check阶段
setImmediate()实际上是一个特殊的定时器,它在事件循环的一个单独的阶段中运行。它使用libuv API来调度回调,以在轮询(poll)阶段完成后执行。
close callback阶段
如果套接字socks或句柄突然关闭(例如socket.destroy()),则在此阶段将发出'close'事件。 否则它将通过process.nextTick()触发事件。
8. setImmediate 与 setTimeout
setImmediate()用于在当前轮询阶段完成后执行脚本。
setTimeout()计划脚本在经过最小阈值(以毫秒为单位)后运行。
定时器执行的顺序取决于它们被调用的上下文。 如果两者都是在主模块内调用的,那么时序将受到进程性能的限制(可能会受到计算机上运行的其他应用程序的影响)。
简言之: setTimediate 和 setTimeout 的执行顺序不确定。
// setTimeout和setImmediate顺序是不固定,看node准备时间
setTimeout(function () {
console.log('setTimeout')
},0);
setImmediate(function () {
console.log('setImmediate')
});
输出的结果可能是这样
setTimeout
setImmediate
也有可能是这样
setImmediate
setTimeout
But, 如果在I / O周期内移动这两个调用,则立即回调总是首先执行, 可以爬楼参考 poll阶段的介绍。
使用setImmediate()的主要优点是,如果在I / O周期内进行调度,将始终在任何计时器之前执行setImmediate(),而不管有多少个计时器。
9. process.nextTick
为什么要用process.nextTick
允许用户处理错误,清理任何不需要的资源,或者可能在事件循环继续之前再次尝试请求。 有时需要在调用堆栈解除之后但事件循环继续之前允许回调运行。
process.nextTick()没有显示在图中,即使它是异步API的一部分。 这是因为process.nextTick()在技术上并不是事件循环的一部分。 相反,nextTickQueue将在当前操作完成后处理,而不管事件循环的当前阶段如何。
回顾一下事件循环机制,只要你在给定的阶段调用process.nextTick(),所有传递给process.nextTick()的回调都将在事件循环继续之前被解析。
// nextTick是队列切换时执行的,timer->check队列 timer1->timer2不叫且
setImmediate(() => {
console.log('setImmediate1')
setTimeout(() => {
console.log('setTimeout1')
}, 0);
})
setTimeout(()=>{
process.nextTick(()=>console.log('nextTick'))
console.log('setTimeout2')
setImmediate(()=>{
console.log('setImmediate2')
})
},0);
在讨论事件循环(Event Loop)的时候,要时刻知道 宏任务,微任务,process.nextTick等概念。 上面代码执行的结果可能为:
setTimeout2
nextTick
setImmediate1
setImmediate2
setTimeout1
或者
setImmediate1
setTimeout2
setTimeout1
nextTick
setImmediate2
为什么呢? 这个就留给各位看官的一个思考题吧。欢迎留言讨论~