翼度科技»论坛 编程开发 JavaScript 查看内容

[NodeJS] NodeJS事件循环

6

主题

6

帖子

18

积分

新手上路

Rank: 1

积分
18
JS是单线程的,如果出现阻塞会严重影响代码执行效率。NodeJS通过事件循环,尽可能地将耗时任务委派给系统内核来实现非阻塞IO。
NodeJS提供了许多和异步相关的API,除了语言标准规定的setTimeout和setInterval,还有setImmediate和process.nextTick。
经常和这几个出现在面试题里的还有Promise.resolve().then()。
事件循环流程

当NodeJS启动时,会先进行事件循环的初始化(事件循环还没开始),会先完成下面的事情:

  • 解析执行同步任务;
  • 发出异步请求;
  • 注册定时器回调;
  • 执行process.nextTick();
然后再开始事件循环。
事件循环的操作顺序如下图所示:
  1.    ┌───────────────────────────┐
  2. ┌─>│           timers          │
  3. │  └─────────────┬─────────────┘
  4. │  ┌─────────────┴─────────────┐
  5. │  │   pending 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.    └───────────────────────────┘
复制代码
名词解释

  • 条件存储:条件存储是一种优化技术。编译器可以将 if 语句编译成一种条件存储操作。这种操作仅在特定条件下才会写入数据,从而避免不必要的写操作。在这段代码中,loop->stop_flag 的值只有在其当前值不为零时才会被修改。这避免了不必要的写操作,因为如果 loop->stop_flag 已经是零,则不需要再写一次零。
  • 缓存行:缓存行是处理器缓存的基本单位,通常为 64 字节。缓存用于存储从内存中加载的数据,以加快访问速度。当处理器需要访问某个内存地址时,会先检查缓存中是否存在对应的数据。如果缓存中存在该数据(称为缓存命中),则可以快速访问;如果不存在(称为缓存未命中),则需要从较慢的主存中加载数据。在现代处理器中,缓存写操作可能会使缓存行变脏(dirty),即缓存中的数据与主存中的数据不一致。每次写操作都可能导致缓存行的变脏和随后的写回操作(将缓存中的数据写回主存),这些操作会影响性能。
通过条件存储,如果 loop->stop_flag 本来就是零,则不会进行写操作,避免了缓存行变脏,从而减少了写回主存的开销,提高了缓存的利用效率。
process.nextTick和Promise

或许你会疑惑上面的事件循环阶段怎么没有讲到process.nextTick和Promise回调(微任务)。
这两个回调的执行时机不在阶段“内部”,而是在阶段“之间”,在每个阶段结束时被执行。
并且,process.nextTick的执行顺序先于Promise回调(微任务)。
微任务除了nextTick和promise,还有MutationObserver和queueMicrotask。
nextTick属于特殊的高优先级微任务,而promise、MutationObserver和queueMicrotask的优先级一致。
MutationObserver是用来监听DOM的,是浏览器独有的;而nextTick是NodeJS独有的;
promise和queueMicrotask在两种环境下都有。
setTimeout和setImmediate

setTimeout在timers阶段执行,setImmediate的回调在check阶段执行,因此setTimeout会早于setImmediate完成。
案例
  1. int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  2.   int timeout;
  3.   int r;
  4.   int can_sleep;
  5.   // 检查事件循环是否还活跃(即是否还有活跃的句柄或请求)
  6.   r = uv__loop_alive(loop);
  7.   if (!r)
  8.     uv__update_time(loop); // 更新事件循环的当前时间
  9.   /* 保持向后兼容性,在进入 UV_RUN_DEFAULT 的 while 循环之前处理定时器。
  10.    * 否则定时器只需执行一次,这应在轮询之后完成,以保持事件循环的正确执行顺序。
  11.    */
  12.   if (mode == UV_RUN_DEFAULT && r != 0 && loop->stop_flag == 0) {
  13.     uv__update_time(loop); // 更新事件循环的当前时间
  14.     uv__run_timers(loop);  // 运行所有到期的定时器 (Timers)
  15.   }
  16.   // 主循环,根据不同的模式执行事件循环
  17.   while (r != 0 && loop->stop_flag == 0) {
  18.     // 检查是否可以进入睡眠状态,即是否有挂起的任务或空闲句柄
  19.     can_sleep =
  20.         uv__queue_empty(&loop->pending_queue) &&
  21.         uv__queue_empty(&loop->idle_handles);
  22.     // 运行挂起的任务 (Pending Callbacks)
  23.     uv__run_pending(loop);
  24.     // 运行空闲句柄和预处理句柄 (Idle Prepare)
  25.     uv__run_idle(loop);
  26.     uv__run_prepare(loop);
  27.     timeout = 0;
  28.     // 根据模式设置超时时间
  29.     if ((mode == UV_RUN_ONCE && can_sleep) || mode == UV_RUN_DEFAULT)
  30.       timeout = uv__backend_timeout(loop);
  31.     // 增加事件循环计数
  32.     uv__metrics_inc_loop_count(loop);
  33.     // 轮询I/O事件 (Poll)
  34.     uv__io_poll(loop, timeout);
  35.     /* 处理立即回调(例如 write_cb)固定次数,以避免循环饥饿。 */
  36.     for (r = 0; r < 8 && !uv__queue_empty(&loop->pending_queue); r++)
  37.       uv__run_pending(loop);
  38.     /*
  39.      * 进行最后一次 provider_idle_time 的更新,以防 uv__io_poll
  40.      * 因超时返回但未接收到任何事件。如果 provider_entry_time 从未设置
  41.      * (即 timeout == 0),或者已经因为接收到事件而更新,则此调用将被忽略。
  42.      */
  43.     uv__metrics_update_idle_time(loop);
  44.     // 运行check句柄 (Check)
  45.     uv__run_check(loop);
  46.     // 运行关闭的回调 (Close Callbacks)
  47.     uv__run_closing_handles(loop);
  48.     // 更新事件循环的当前时间和运行所有到期的定时器 (Timers)
  49.     uv__update_time(loop);
  50.     uv__run_timers(loop);
  51.     // 检查事件循环是否还活跃
  52.     r = uv__loop_alive(loop);
  53.     if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
  54.       break; // 如果模式为 UV_RUN_ONCE 或 UV_RUN_NOWAIT,则退出循环
  55.   }
  56.   /* 这个 if 语句让 gcc 将其编译为条件存储。避免弄脏缓存行。 */
  57.   if (loop->stop_flag != 0)
  58.     loop->stop_flag = 0; // 清除停止标志
  59.   return r; // 返回事件循环是否还活着
  60. }
复制代码
理论上上面这段代码会先输出1再输出2,但实际是顺序不确定。
因为在NodeJS中,setTimeout的第二个参数delay缺省值为1,根据官方文档,这个参数的取值范围为1到2147483647之间,超出这个范围会被设置为1,而非整数会被截去小数部分变为整数。
并且实际执行的时候,进入事件循环之后,可能到了1毫秒,也可能还没到,因此timers阶段的队列可能是空的,于是就先执行了check阶段的setImmediate回调,而到了下一阶段,才是setTimeout的回调。
另一个案例
  1. setTimeout(()=>console.log(1));
  2. setImmediate(()=>console.log(2));
复制代码
这个例子中,则一定是先输出2,然后才是1.
因为readFile的回调会在pending I/O callbacks阶段被执行,此时的setTimeout回调最快也只能在下一个loop中被执行,而setImmediate的回调被添加到check阶段的队列,当当前这个loop执行到check阶段的时候,就会被执行。
测试题
  1. const fs = require('fs');
  2. fs.readFile('test.js', () => {
  3.   setTimeout(() => console.log(1));
  4.   setImmediate(() => console.log(2));
  5. });
复制代码
答案
  1. setImmediate(() => {
  2.   console.log(1)
  3.   setTimeout(() => {
  4.     console.log(2)
  5.   }, 100)
  6.   setImmediate(() => {
  7.     console.log(3)
  8.   })
  9.   process.nextTick(() => {
  10.     console.log(4)
  11.   })
  12. })
  13. process.nextTick(() => {
  14.   console.log(5)
  15.   setTimeout(() => {
  16.     console.log(6)
  17.   }, 100)
  18.   setImmediate(() => {
  19.     console.log(7)
  20.   })
  21.   process.nextTick(() => {
  22.     console.log(8)
  23.   })
  24. })
  25. console.log(9)
复制代码
解析

  • 同步代码:注册setImmediate,等待事件循环到达check阶段;注册nextTick回调;同步代码输出9;
  • 事件循环启动后,在到达check阶段之前nextTick肯定是先被执行的,于是先输出5;输出之后依次注册setTimeout,setImmediate和nextTick;
  • 在到达check阶段之前的阶段之间,nextTick回调被再次执行,输出8;
  • 中间阶段的队列都是空的,直到事件循环来到check阶段,执行最顶层的setImmediate回调,先输出1,然后依次注册setTimeout,setImmediate,nextTick回调;
  • 离开setImmediate,再次执行nextTick回调,输出4;
  • 到达timers阶段,但是通常这时候还没到达100ms,于是跳过;
  • 再次到达check阶段,输出队列中的7和3;
  • 在下次循环的poll阶段等待,直到定时器完成,依次输出6和2。
参考文章

[1] Node 定时器详解 - 阮一峰的网络日志
[2] The Node.js Event Loop
[3] Understanding process.nextTick()
[4] Understanding setImmediate()

来源:https://www.cnblogs.com/feixianxing/p/18284315/nodejs-event-loop
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具