参考

有空需要自己把英文文档翻译一遍,锻炼英语能力。

event loop 这玩意儿对于前端来说已经超纲了,这东西是 c++ 实现的,js 程序员怎么会知道 c++ 的东西。那该怎么学的,看官方文档。

首先讲讲操作系统有关的知识

当我们按下键盘的时候,发生了什么, 操作系统是怎么知道的?

到现在我也不知道他是怎么知道的,通过搜索发现是这样的:键盘下面有一个电路,当我们按下某个键的时候,就有触发一个信息,例如:0101,这个数字就会被传给操作系统,操作系统知道了以后,就会传给浏览器,浏览器知道了以后,把内容显示在 input 框里。

为什么讲这个呢,浏览器会接受到系统给他的 事件,这个是第一个概念,操作系统会接受到各种信号(像插入 usb 之类的事件,但就不是通知浏览器了,而是通知其他设备),在分配给其他软件。

这其中又有一个疑问,操作系统是在接受这个信号的时候,是立马就知道的呢?, 还是每隔一段时间问一次呢?

非常遗憾,操作系统并没有那么智能,他只能不停的等键盘触发,比如每隔5mm,看看键盘有没有触发,不停地循环,当用户按了以后就放进一个队列,操作系统每隔5mm就会发现并执行,这个行为就叫 轮询

接下来看看JS

浏览器不止运行JS,还要发起一些网络请求,比如:当浏览器执行JS代码的时候,遇到中间有一个AJAX请求,需要耗时0.2s, JS是一个单线程的,单线程就是不可能同时执行两个任务的,所以应该要等ajax发送接受完在继续执行接下来的代码呢?还是继续执行以后的代码,在回头接受AJAX呢?**, 两个方向只能二选一。

通过认知我们知道,JS选得是第二条路,先执行接下来的代码,但是疑问又来了,请问这0.2s是谁在等(轮询,开头说过没那么智能)呢,谁知道这个请求成功了呢?总得有人在等这个请求成功了没把,这段时间JS在做其他事情,首先排除JS。

所以是 **C++**,写浏览器的核心机制(这对前端来说已经超纲了),不停地去看网络到了没,或者是操作系统,不管是谁在做这个事情,反正不是JS,暂时不管这些细节。

所以这个轮询是不是该遵循一种机制呢?(得有规律的告诉JS网络请求到了把),这不分知识点(超纲,已经脱离了 js)就是我们需要搞明白的 eventloop。

接下来我们就讲讲这个规则

当 JS 遇到一个异步任务的时候,其实JS什么都没做,他只是给 C++ 发了一个消息,然后继续做自己的事情(单线程),C++ 在忙的时候,有一定的规则、顺序。然后把AXAJ返回的时间告诉 JS,JS 再继续执行。

现在来看 node.js,nodeJs可以执行JS代码,浏览器也能执行JS代码,是差不多的,但是 event loop 是 nodeJS 的概念(node 官方的解释链接,但在学 JS 的时候从来没提到这个概念),而不是浏览器的, 所以我们这里讲讲 nodeJS 的 event loop。

那么这是个啥呢?是真实存在的还是虚拟的概念。
翻译成中文就是 事件循环,就像组件的生命周期一样或者是人生循环(生 -> 长 -> 成熟 -> 老 -> 病 -> 死 -> 投胎),按照佛教的观念什么时候尽头呢?什么时候开始呢?人是先生出来还是先投胎呢?
这是个抽象的概念,人生循环字都认识,组成一起问我人生循环是什么那就不认识了。其实人生循环就是人在一生中处于不同阶段的过程。
所以事件循环是指多个阶段的交替。

当 Node.js 启动时,会做这几件事

  1. 初始化 event loop
  2. 开始执行脚本(或者进入 REPL,本文不涉及 REPL)。这些脚本有可能会调用一些异步 API、设定计时器或者调用 process.nextTick()
  3. 开始处理 event loop

如何处理 event loop 呢?下图给出了一个简单的概览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   ┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

其中每个方框都是 event loop 中的一个阶段。

为了前端理解、简化为三个阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   ┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ /*I/O callbacks*/ │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ /*idle, prepare*/ │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
└──┤ check
└──────────┬────────────┘
┌──────────┴────────────┐
│ /*close callbacks*/ │
└───────────────────────┘

其中 poll 阶段会停留一段时间(如网络请求、文件回调)

分析:当我们执行 setTimeout(fn, 1000) 的时候,event loop 有没有开启?也就是先开启 event loop 还是,先执行代码呢?

从原文中看到,这并不确定(开启event loop(开启一个进程),执行JS(调用 V8 引擎)都需要时间,如:先开启了 event loop,但JS还没开始执行)。

再来看这个 setTimeout(fn, 1000),当 js 执行的时候,遇到 setTimeout(题外:这个 api 是浏览器提供的,js 本身是没有的)就把这个任务(fn)放进 timers 的队列中,然后就继续执行自己的其他事情,这个时候 timers 开启了也可能没有开启,一般最大的可能会在 poll 阶段,这个时候在等,边等边看时间,等了 300ms 的时候还暂不需要去执行 timers 里面的任务,500ms 的时候同理…,直至 1000ms 的时候就不等了,赶紧经过 check 阶段(必须),然后到达 timer 阶段执行掉任务,然后又回到 poll 继续等。
但是这个等待是有最长时间,不可能一直等下去的,比如说最多等待 3s,超过 3s 还没有任务就继续下个阶段。

说了 poll 阶段和 timers 阶段,那么 check 阶段有什么呢?在 node 中有一个 setImmediate。他跟 setTimeout 差不多,但是后面没有参数。

1
2
3
4
5
6
7
8
9
// 当遇到这样的代码的时候
setTimeout(fn, 1000) // 执行并将 fn 放进 timer 队列
setImmediate(fn2) // 执行并将 fn2 放进 check 队列

1. timers [fn(1000ms)]
2. poll (等待....)
3. check [fn2]

// 此时 poll 阶段就不会继续等待更长的时间了(马上就有任务等着做了,还等个球)
所以。这才有了一道著名的考题:setTimeout(fn, 0) 和 setImmediate(fn2)【setImmediate属于check阶段】,fn先执行还是fn2先执行?

以前我们都是靠背,setImmediate 优先级高,先执行。但是从上文分析,答案是不确定哪个先执行,得看event loop的开启时间,如果event loop 在 timer 阶段,那就会立马执行setTimeout 函数,再执行 setImmediate,如果在 poll 阶段,则会先执行setImmediate 再执行 setTimeout,当然也就只有刚开始的时候才会出现这种情况(因为不确定event loop 什么时候会开启),但是当一切都准备就绪的时候,即:

1
2
3
4
setTimeout(() => {
setTimeout(fn, 0)
setImmediate(fn2)
}, 1000)

肯定就是 setImmediate 会先执行,因为大部分时间都会停留在poll阶段,所以也就有了我们平时记的 setImmediate 优先级高。

setTimeout(fn, 1000) 真的会准时在 1000ms 后执行吗?答案是不一定,原因是必须经过 check 阶段才到 timer 阶段去执行 fn。

这里再扯一个process.nextTick,他不属于任何一个阶段,代表在某个阶段立马执行,如:

1
2
3
4
5
6
7
setTimeout(() => {
process.nextTick(fn3)
console.log(1)
setTimeout(fn, 0)
setImmediate(fn2)
}, 1000)
// fn3 => fn2 => fn
1
2
3
4
5
6
7
8
9
setTimeout(() => {
process.nextTick(fn4)
setTimeout(() => {
fn()
process.nextTick(fn3)
}, 0)
setImmediate(fn2)
}, 1000)
// fn4 => fn2 => fn => fn3

我买再来看一题

共四个函数:

第一轮执行,fn1放进check队列,fn2放进timer队列

  1. timer [fn2]
  2. poll
  3. check [fn1]

执行 fn1 的时候先打印出了 setImmediate1 ,然后遇到setTimeout 函数即 fn3,所以把 fn3 放进 timer 队列,event loop 执行完了check 里的任务,进入下一个 timer 阶段,

  1. timer [fn2, fn3]
  2. poll
  3. check [fn1]

所以,在 timer 阶段 fn2 先执行,打印出了 setTimeout2,然后把 fn4 放入 check 队列,注意:此时 timer 阶段尚未结束,必须先执行完 timer 阶段所有函数才能才能进入下一个阶段,所以紧接着打印出 setTimeout1,最后打印出 setImmediate2

  1. timer [fn2, fn3]
  2. poll
  3. check [fn1, fn4]

照着这张图,无脑解决所有 event loop 的题目。

讲了半天都是 node 中的 event loop,那么跟 jser 有什么关系呢?浏览器比较简单,node 算比较完整的 event loop,会了 node 再看浏览器中就可以信手拈来了。

浏览器,就相对来说比较简单了,除了同步代码,就是异步代码(宏任务–(马上)、微任务–(一会))

macrotasks:setTimeout,setInterval, setImmediate, I/O, UI渲染
microtasks:Promise.then(大部分都是用process.nextTick实现的), process.nextTick Object.observe(已经没人用了,取而代之的是 MutationObserver)

看题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function async1() {
console.log(1);
await async2();
console.log(2);
}

async function async2() {
console.log(3);
}

async1();

new Promise(function (resolve) {
console.log(4);
resolve();
}).then(function () {
console.log(5)
})

解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function async1() {
console.log(1);
await async2();
console.log(2); // 等会儿再执行,标记为 f1
}

async function async2() {
console.log(3);
}

async1();

new Promise(function (resolve) {
console.log(4);
resolve();
}).then(function () {
console.log(5) // 等会儿再执行,标记为 f2
})

// 同步任务结束,依次开始执行剩下的 f1,f2。

总结:Eventloop

  1. eventloop 是指一些阶段,在浏览器中(2个)和在 node.js(简化为3个)中实现是不一样的。
  2. setTimeout -> timers 阶段,setImmediate -> check 阶段,nextTick -> 当前阶段的后面。
  3. promise 是对 nextTick 的封装。