事件循环 EventLoop 学习笔记

327 阅读2分钟
  • 先抛一块砖

    setTimeout(() => console.log(1));
    setImmediate(() => console.log(2));
    process.nextTick(() => console.log(3));
    Promise.resolve().then(() => console.log(4));
    (() => console.log(5))();
    

    执行结果:

    5
    3
    4
    1
    2
    

node事件分 同步、异步,由于JS的单线程原理,异步事件的处理方式依靠 事件循环。

  • 异步事件分为:微事件/宏事件
  • 微事件包括:
    • process.nextTick
    • Promise.then
  • 宏事件包括:
    • setTimeout/setInterval
    • setImmediate
    • I/O callback

不同的异步事件,循环规则也不同。


宏事件循环

  1. timers阶段:setTimeout/setInterval的回调函数执行阶段
  2. I/O callbacks阶段:未解决的回调执行执行
  3. poll阶段:轮询阶段。接受新的I/O回调,并将其加入轮询列队中进行先进先出的顺序执行。
    • 若该阶段事件列队为空,检查check阶段是否有事件列队,有则顺序执行。同时也会检查是否有定时器到达阈值,如果有则循环绕到timers阶段时执行。如果没有其他异步任务要处理会一直停留在该阶段等待 I/O 结果返回。
    • 若该阶段的事件列队不为空,则将事件列队中的事件执行完直到为空,或直到内存溢出
  4. check阶段:执行setImmediate()的回调函数
  5. close callbacks:执行关闭请求的回调函数 .on('close', callback)

微事件循环

  1. process.nextTick
  2. Promise.then

在同一次循环中process.nextTick总要比Promise.then先执行

循环原则

  • 只有前一个阶段的事件队列全部清空以后,才会执行下一个阶段的事件队列
  • 进入到下一个阶段的事件列队之前,会先执行微事件列队直到为空。
  • 同一次循环中,微事件的执行总是优先于宏事件

事件执行顺序:

  1. 同步事件
  2. 发出异步请求
  3. 规划定时器生效的时间
  4. process.nextTick
  5. Promise.then
  6. 事件循环

与 浏览器 环境的EventLoop不同

浏览器环境的EventLoop代码解释

// 事件循环 => 主线程
while(macroQueue.waitForMessage()){
  // 1. 执行完调用栈上当前的同步任务
  // call stack

  // 2. 遍历微任务列队,直到把微任务列队上的所有任务都执行完毕(清空微任务列队)
  // 注意:微任务可以添加新的微任务到当前队列中
  for(let i = 0; i<microQueue.length; i++){
    // 获取并执行下一个微任务(先进先出)
    microQueue[i].processNextMessage()
  }

  // 3. 渲染(渲染线程)

  // 4. 从宏任务中取出下一个宏任务,继续循环
  macroQueue.processNextMessage();
} 

主要区别在于微任务列队的执行

  • 浏览器环境的EventLoop会在每一个 宏事件 执行完成之后,遍历微任务列队中的所有微任务进行执行
  • node环境的EventLoop则是在 每个阶段中的列队 的所有事件执行完成之后,并且在进入到下一个阶段之前 执行微任务列队中的微任务

微任务的作用

  • 减少操作延迟
  • 使代码在 当前宏任务同步代码后 及 下一个宏任务前 执行
  • 可实现批量操作,可以看下面这个有趣的栗子🌰
function setup(){
  let messageQueue = [];
  return (message) => {
    messageQueue.push(message);
    // 加上 messageQueue.length === 1 的判断是为了queueMicrotask() 只被执行一次
    if (messageQueue.length === 1) {
      // queueMicrotask 将在当前宏任务下 callStack执行栈中的同步任务执行结束后 才开始执行
      queueMicrotask(() => {
        const json = JSON.stringify(messageQueue);
        messageQueue.length = 0;
        console.log(json);
      })
    }
  }
}

queueMicrotask(()=>{
  console.log('queueMicrotask')
})

const sendMessage = setup();

sendMessage("刘备")
sendMessage("关羽") 
sendMessage("曹操")
// 至此当前callStack调用栈中的同步任务执行完毕,将执行 queueMicrotask 微任务

// setTimeout将放到下一个宏任务进行执行
setTimeout(() => {
  sendMessage("诸葛亮")
});

终极挑战

setTimeout(() => {
  process.nextTick(() => {
    console.log(1)
    process.nextTick(() => console.log(2))
  });
  console.log(3)
  setTimeout(() => { console.log(4)})
  setImmediate(() => console.log(5));
});
setTimeout(() => {
  process.nextTick(() => console.log(6));
  Promise.resolve(7).then(res=>console.log(res))
  console.log(8)
});
(() => console.log(9))();
setImmediate(() => console.log(10));

你答对了吗?

9
3
1
2
8
6
7
10
5
4

参考链接

Node 定时器详解——阮一峰

Node.js event loop workflow & lifecycle in low level