Event loop

众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点),当然可以引入读写锁解决这个问题。

JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。

  1. console.log('script start');
  2. setTimeout(function() {
  3. console.log('setTimeout');
  4. }, 0);
  5. console.log('script end');

以上代码虽然 setTimeout 延时为 0,其实还是异步。这是因为 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增加。所以 setTimeout 还是会在 script end 之后打印。

不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task

  1. console.log('script start');
  2. setTimeout(function() {
  3. console.log('setTimeout');
  4. }, 0);
  5. new Promise((resolve) => {
  6. console.log('Promise')
  7. resolve()
  8. }).then(function() {
  9. console.log('promise1');
  10. }).then(function() {
  11. console.log('promise2');
  12. });
  13. console.log('script end');
  14. // script start => Promise => script end => promise1 => promise2 => setTimeout

以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务,所以会有以上的打印。

微任务包括 process.nextTickpromiseObject.observeMutationObserver

宏任务包括 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。

所以正确的一次 Event loop 顺序是这样的

  1. 执行同步代码,这属于宏任务
  2. 执行栈为空,查询是否有微任务需要执行
  3. 执行所有微任务
  4. 必要的话渲染 UI
  5. 然后开始下一轮 Event loop,执行宏任务中的异步代码

通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的 界面响应,我们可以把操作 DOM 放入微任务中。

Node 中的 Event loop

Node 中的 Event loop 和浏览器中的不相同。

Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行

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

timer

timers 阶段会执行 setTimeoutsetInterval

一个 timer 指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的事务而延迟。

下限的时间有一个范围:[1, 2147483647] ,如果设定的时间不在这个范围,将被设置为1。

I/O

I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调

idle, prepare

idle, prepare 阶段内部实现

poll

poll 阶段很重要,这一阶段中,系统会做两件事情

  1. 执行到点的定时器
  2. 执行 poll 队列中的事件

并且当 poll 中没有定时器的情况下,会发现以下两件事情

  • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制
  • 如果 poll 队列为空,会有两件事发生
    • 如果有 setImmediate 需要执行,poll 阶段会停止并且进入到 check 阶段执行 setImmediate
    • 如果没有 setImmediate 需要执行,会等待回调被加入到队列中并立即执行回调

如果有别的定时器需要被执行,会回到 timer 阶段执行回调。

check

check 阶段执行 setImmediate

close callbacks

close callbacks 阶段执行 close 事件

并且在 Node 中,有些情况下的定时器执行顺序是随机的

  1. setTimeout(() => {
  2. console.log('setTimeout');
  3. }, 0);
  4. setImmediate(() => {
  5. console.log('setImmediate');
  6. })
  7. // 这里可能会输出 setTimeout,setImmediate
  8. // 可能也会相反的输出,这取决于性能
  9. // 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
  10. // 否则会执行 setTimeout

当然在这种情况下,执行顺序是相同的

  1. var fs = require('fs')
  2. fs.readFile(__filename, () => {
  3. setTimeout(() => {
  4. console.log('timeout');
  5. }, 0);
  6. setImmediate(() => {
  7. console.log('immediate');
  8. });
  9. });
  10. // 因为 readFile 的回调在 poll 中执行
  11. // 发现有 setImmediate ,所以会立即跳到 check 阶段执行回调
  12. // 再去 timer 阶段执行 setTimeout
  13. // 所以以上输出一定是 setImmediate,setTimeout

上面介绍的都是 macrotask 的执行情况,microtask 会在以上每个阶段完成后立即执行。

  1. setTimeout(()=>{
  2. console.log('timer1')
  3. Promise.resolve().then(function() {
  4. console.log('promise1')
  5. })
  6. }, 0)
  7. setTimeout(()=>{
  8. console.log('timer2')
  9. Promise.resolve().then(function() {
  10. console.log('promise2')
  11. })
  12. }, 0)
  13. // 以上代码在浏览器和 node 中打印情况是不同的
  14. // 浏览器中一定打印 timer1, promise1, timer2, promise2
  15. // node 中可能打印 timer1, timer2, promise1, promise2
  16. // 也可能打印 timer1, promise1, timer2, promise2

Node 中的 process.nextTick 会先于其他 microtask 执行。

  1. setTimeout(() => {
  2. console.log("timer1");
  3. Promise.resolve().then(function() {
  4. console.log("promise1");
  5. });
  6. }, 0);
  7. process.nextTick(() => {
  8. console.log("nextTick");
  9. });
  10. // nextTick, timer1, promise1