event-loop
Overview
宿主环境(Hosting Environment),比如浏览器、Node.js,提供 WEB APIs。当这些 WEB API 触发之时,将 Task 加入 Callback Queue。
当 Call Stack 为空时,Event Loop将 Callback Queue 中的 Task,加入到 Call Stack中。每次加入的Task,称之为一个Tick。
JS 引擎(JS Engine),执行 Call Stack 中的Tasks。

Job Queue
由上文可以看出,Event Loop,是Hosting Environment提供的功能。ES6中,由于Promise机制引入标准,Event Loop成为JS engine的一部分,而不再仅仅是 Hosting Environment 的职责。
JS Engine 将 Job Queue 加到当前 Tick 的最后。优先于下一个Tick,也就是Callback Queue中的 code。"later, but as soon as possible."
很多文章,将此类任务,称之为微任务(MicroTask)。为了区分,Event Loop 中的Task,成为宏任务(MacroTask)。
WEB APIs
事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等。
MacroTask
script(整体代码), setTimeout, setInterval, setImmediate(node 独有), I/O, UI rendering
MicroTask
process.nextTick(node 独有), Promises, Object.observe(废弃), MutationObserver
实现
根据上下文的不同,Event loop也有不同的实现:
Node.js使用了 libuv 库来实现 Event loop;- 浏览器中,
html规范定义了Event loop,具体的实现则交给不同的厂商去完成。
Browser
同一个 context 中,总的执行顺序为:同步代码—>microTask—>macroTask。
浏览器中,一个事件循环里有很多个来自不同任务源的任务队列(task queues),每一个任务队列里的任务是严格按照先进先出的顺序执行的。但是,因为浏览器自己调度的关系,不同任务队列的任务的执行顺序是不确定的。大体步骤如下:
- 浏览器会不断从
Task队列(Callback Queue)中按顺序取Task执行 - 每执行完一个
Task都会检查Microtask队列是否为空(执行完一个Task的具体标志是Call Stack为空) - 如果不为空则会一次性执行完所有
Microtask。 - 然后再进入下一个循环去 task 队列中取下一个
Task执行,以此类推。

Node.js
Node.js的Event Loop分为 6 个阶段,它们会按照顺序反复运行,分别如下:
timers:执行setTimeout()和setInterval()中到期的Callback。I/O callbacks:上一轮循环中有少数的I/O callback会被延迟到这一轮的这一阶段执行idle, prepare:队列的移动,仅内部使用poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段check:执行setImmediate的callbackclose callbacks:执行close事件的callback,例如socket.on("close", func)
不同于浏览器的是,在每个阶段完成后,而不是MacroTask任务完成后,microTask队列就会被执行。这就导致了同样的代码在不同的上下文环境下会出现不同的结果。
另外需要注意的是,如果在timers阶段执行时创建了setImmediate则会在此轮循环的check阶段执行,如果在timers阶段创建了setTimeout,由于timers已取出完毕,则会进入下轮循环,check阶段创建timers任务同理。

执行顺序差别
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
浏览器输出:
time1
promise1
time2
promise2
Node输出:
time1
time2
promise1
promise2
浏览器:两个setTimeout作为两个MacroTask, 所以先输出timer1, promise1,再输出timer2,promise2。
Node:最初timer1和timer2就在timers阶段中。开始时首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;
至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2。
setImmediate(() => {
console.log('timer1')
Promise.resolve().then(function () {
console.log('promise1')
})
})
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function () {
console.log('promise2')
})
}, 0)
Node输出:
timer1 timer2
promise1 或者 promise2
timer2 timer1
promise2 promise1
按理说setTimeout(fn,0)应该比setImmediate(fn)快,应该只有第二种结果,为什么会出现两种结果呢?
这是因为Node做不到 0 毫秒,最少也需要 1 毫秒。实际执行的时候,进入事件循环以后,有可能到了 1 毫秒,也可能还没到 1 毫秒,取决于系统当时 的状况。如果没到 1 毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。
另外,如果已经过了Timer阶段,那么setImmediate会比setTimeout更快,例如:
const fs = require('fs');
fs.readFile('test.js', () => {
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
});
上面代码会先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 timers 阶段。因此,setImmediate 才会早于 setTimeout 执行。
process.nextTick优先于Promise
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
Promise.resolve().then(() => console.log(3));
process.nextTick(() => console.log(4));
输出结果:4 3 1 2或者4 3 2 1
microTask优于macroTask运行,所以先输出下面两个;
而在Node中process.nextTick比Promise更加优先,所以 4 在 3 前;
而根据我们之前所说的Node没有绝对意义上的 0ms,所以 1, 2 的顺序不固定。
并发
通过单线程事件循环实现
等待条件以及竞态条件实现:
// gate
if (a && b) {
// do something here
}
if (!a) {
// do something here
}
不要使用 Synchronous Ajax Request。
async function expression,用于匿名函数,立即执行函数。