首页 > 编程语言 >[NodeJS] NodeJS事件循环

[NodeJS] NodeJS事件循环

时间:2024-07-04 17:42:22浏览次数:21  
标签:__ nextTick setImmediate NodeJS uv 循环 事件 loop

JS是单线程的,如果出现阻塞会严重影响代码执行效率。NodeJS通过事件循环,尽可能地将耗时任务委派给系统内核来实现非阻塞IO。

NodeJS提供了许多和异步相关的API,除了语言标准规定的setTimeoutsetInterval,还有setImmediateprocess.nextTick

经常和这几个出现在面试题里的还有Promise.resolve().then()

事件循环流程

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

  1. 解析执行同步任务;
  2. 发出异步请求;
  3. 注册定时器回调;
  4. 执行process.nextTick()

然后再开始事件循环。

事件循环的操作顺序如下图所示:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │   pending I/O callbacks   │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

每一个方框对对应着事件循环的一个阶段(phase),每一个阶段有一个先进先出的回调队列需要执行。

当事件循环进入到其中一个阶段时,它会依次执行并尝试清空队列中的回调任务,当队列被清空或者回调执行数量达到最大限制时,事件循环会进入到下一个阶段。

  1. timers:定时器阶段,执行setTimeoutsetInterval的回调函数;
  2. pending I/O callbacks:除了定时器回调、setImmediate回调和关闭回调,其它回调都在这里执行;
  3. idle, prepare:这个阶段只供libuv内部调用;
  4. Poll:这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。
  5. check:执行setImmediate回调;
  6. close callbacks:执行关闭请求的回调函数,比如socket.on('close', ...)

事件循环的源码解析

源码位置:

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int can_sleep;

  // 检查事件循环是否还活跃(即是否还有活跃的句柄或请求)
  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop); // 更新事件循环的当前时间

  /* 保持向后兼容性,在进入 UV_RUN_DEFAULT 的 while 循环之前处理定时器。
   * 否则定时器只需执行一次,这应在轮询之后完成,以保持事件循环的正确执行顺序。
   */
  if (mode == UV_RUN_DEFAULT && r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop); // 更新事件循环的当前时间
    uv__run_timers(loop);  // 运行所有到期的定时器 (Timers)
  }

  // 主循环,根据不同的模式执行事件循环
  while (r != 0 && loop->stop_flag == 0) {
    // 检查是否可以进入睡眠状态,即是否有挂起的任务或空闲句柄
    can_sleep =
        uv__queue_empty(&loop->pending_queue) &&
        uv__queue_empty(&loop->idle_handles);

    // 运行挂起的任务 (Pending Callbacks)
    uv__run_pending(loop);
    // 运行空闲句柄和预处理句柄 (Idle Prepare)
    uv__run_idle(loop);
    uv__run_prepare(loop);

    timeout = 0;
    // 根据模式设置超时时间
    if ((mode == UV_RUN_ONCE && can_sleep) || mode == UV_RUN_DEFAULT)
      timeout = uv__backend_timeout(loop);

    // 增加事件循环计数
    uv__metrics_inc_loop_count(loop);

    // 轮询I/O事件 (Poll)
    uv__io_poll(loop, timeout);

    /* 处理立即回调(例如 write_cb)固定次数,以避免循环饥饿。 */
    for (r = 0; r < 8 && !uv__queue_empty(&loop->pending_queue); r++)
      uv__run_pending(loop);

    /*
     * 进行最后一次 provider_idle_time 的更新,以防 uv__io_poll
     * 因超时返回但未接收到任何事件。如果 provider_entry_time 从未设置
     * (即 timeout == 0),或者已经因为接收到事件而更新,则此调用将被忽略。
     */
    uv__metrics_update_idle_time(loop);

    // 运行check句柄 (Check)
    uv__run_check(loop);
    // 运行关闭的回调 (Close Callbacks)
    uv__run_closing_handles(loop);

    // 更新事件循环的当前时间和运行所有到期的定时器 (Timers)
    uv__update_time(loop);
    uv__run_timers(loop);

    // 检查事件循环是否还活跃
    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break; // 如果模式为 UV_RUN_ONCE 或 UV_RUN_NOWAIT,则退出循环
  }

  /* 这个 if 语句让 gcc 将其编译为条件存储。避免弄脏缓存行。 */
  if (loop->stop_flag != 0)
    loop->stop_flag = 0; // 清除停止标志

  return r; // 返回事件循环是否还活着
}

名词解释

  • 条件存储:条件存储是一种优化技术。编译器可以将 if 语句编译成一种条件存储操作。这种操作仅在特定条件下才会写入数据,从而避免不必要的写操作。在这段代码中,loop->stop_flag 的值只有在其当前值不为零时才会被修改。这避免了不必要的写操作,因为如果 loop->stop_flag 已经是零,则不需要再写一次零。

  • 缓存行:缓存行是处理器缓存的基本单位,通常为 64 字节。缓存用于存储从内存中加载的数据,以加快访问速度。当处理器需要访问某个内存地址时,会先检查缓存中是否存在对应的数据。如果缓存中存在该数据(称为缓存命中),则可以快速访问;如果不存在(称为缓存未命中),则需要从较慢的主存中加载数据。在现代处理器中,缓存写操作可能会使缓存行变脏(dirty),即缓存中的数据与主存中的数据不一致。每次写操作都可能导致缓存行的变脏和随后的写回操作(将缓存中的数据写回主存),这些操作会影响性能。

通过条件存储,如果 loop->stop_flag 本来就是零,则不会进行写操作,避免了缓存行变脏,从而减少了写回主存的开销,提高了缓存的利用效率。

process.nextTick和Promise

或许你会疑惑上面的事件循环阶段怎么没有讲到process.nextTickPromise回调(微任务)。

这两个回调的执行时机不在阶段“内部”,而是在阶段“之间”,在每个阶段结束时被执行。

并且,process.nextTick的执行顺序先于Promise回调(微任务)。

微任务除了nextTickpromise,还有MutationObserverqueueMicrotask

nextTick属于特殊的高优先级微任务,而promiseMutationObserverqueueMicrotask的优先级一致。

MutationObserver是用来监听DOM的,是浏览器独有的;而nextTickNodeJS独有的;

promisequeueMicrotask在两种环境下都有。

setTimeout和setImmediate

setTimeouttimers阶段执行,setImmediate的回调在check阶段执行,因此setTimeout会早于setImmediate完成。

案例

setTimeout(()=>console.log(1));
setImmediate(()=>console.log(2));

理论上上面这段代码会先输出1再输出2,但实际是顺序不确定。

因为在NodeJS中,setTimeout的第二个参数delay缺省值为1,根据官方文档,这个参数的取值范围为12147483647之间,超出这个范围会被设置为1,而非整数会被截去小数部分变为整数。

并且实际执行的时候,进入事件循环之后,可能到了1毫秒,也可能还没到,因此timers阶段的队列可能是空的,于是就先执行了check阶段的setImmediate回调,而到了下一阶段,才是setTimeout的回调。

另一个案例

const fs = require('fs');

fs.readFile('test.js', () => {
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
});

这个例子中,则一定是先输出2,然后才是1.

因为readFile的回调会在pending I/O callbacks阶段被执行,此时的setTimeout回调最快也只能在下一个loop中被执行,而setImmediate的回调被添加到check阶段的队列,当当前这个loop执行到check阶段的时候,就会被执行。

测试题

setImmediate(() => {
  console.log(1)
  setTimeout(() => {
    console.log(2)
  }, 100)
  setImmediate(() => {
    console.log(3)
  })
  process.nextTick(() => {
    console.log(4)
  })
})
process.nextTick(() => {
  console.log(5)
  setTimeout(() => {
    console.log(6)
  }, 100)
  setImmediate(() => {
    console.log(7)
  })
  process.nextTick(() => {
    console.log(8)
  })
})
console.log(9)

答案

往下滑


















9 5 8 1 4 7 3 6 2

解析

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

参考文章

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

标签:__,nextTick,setImmediate,NodeJS,uv,循环,事件,loop
From: https://www.cnblogs.com/feixianxing/p/18284315/nodejs-event-loop

相关文章

  • 【web APIs】快速上手Day03(Dom事件进阶)
    目录WebAPIs-第3天全选文本框案例事件流事件捕获事件冒泡阻止冒泡解绑事件on事件方式解绑addEventListener方式解绑注意事项-鼠标经过事件的区别两种注册事件的区别事件委托综合案例-tab栏切换改造其他事件页面加载事件元素滚动事件页面滚动事件-获取位置页面滚动......
  • 解释下什么是事件代理?应用场景?
    一、是什么事件代理,俗地来讲,就是把一个元素响应事件(click、keydown......)的函数委托到另一个元素前面讲到,事件流的都会经过三个阶段:捕获阶段->目标阶段->冒泡阶段,而事件委托就是在冒泡阶段完成事件委托,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真......
  • java 事件回调的写法,使用回调接口方式
    java编写时,尤其是先用C#语言后转成java的,在编程时一定会遇到,java中没有委托事件的概念。那主类App.java类中实例了一个A对象,那A对象因为某种原因触发了一个事件,想回调App.java中的一个函数,应该怎么写呢?在java中有多有方法来实现,这里讲下回调接口方式,我感觉这种方式比较好理解......
  • selenium08_鼠标事件、键盘事件
    1.鼠标事件需要导入:fromselenium.webdriver.common.action_chainsimportActionChains1)右击el=driver.find_element_by_id("kw")#定位元素ActionChains(driver).context_click(el).perform()#右击2)双击el= driver.find_element_by_xpath("//div[@id='qrcode�......
  • nodejs删除和重新安装
    若重新安装nodejs本人使用卸载并重新安装的方法,简单暴力卸载1.找到以前安装nodejs的文件路径,直接删除2.例如我的在D盘路径,直接卸载3.然后删除配置环境:右键此电脑——属性——高级系统设置——高级——环境变量4.找到用户变量在path关于node与npm并删除5.系统变......
  • 点击事件不生效选择不到,元素被遮挡点击不起作用
    解决方案:两种方案:假设:外层遮挡的类名为:outer,被遮挡的类名为:Inner。1:在不破坏原有样式的基础上增加position:relative;然后z-index控制谁在上面即可.outer{ position:relative;z-index:1;}.Inner{ position:relative;z-index:2;}2.如果点击事件还不......
  • nodejs的安装及使用
    node官网:Node.js中文网、Node.js官网node安装包下载:下载|Node.js中文网、DownloadNode.js®、node的安装法1:直接下载安装node打开下载好的安装程序->接受许可协议、选择安装路径(默认c盘)->Install完成安装法2:通过nvm安装具体参照:nvm的安装及使用-CSDN博客注意......
  • c#循环小练习之最后的练习
    不死神兔最开始有一对小兔小兔子一个月成长为中兔中兔成长一个月为大兔并且生下一对小兔兔子不会死问一年一共有多少只兔子分别输出显示最后的c#循环小练习咯上代码intxiao=1;intzhong=0;intda=0;......
  • 深入解析 Laravel 事件系统:架构、实现与应用
    Laravel的事件系统是框架中一个强大且灵活的功能,它允许开发者在应用程序中定义和使用自定义事件和监听器。这个系统基于观察者模式,使得代码解耦和可维护性大大提高。在本文中,我们将深入探讨Laravel事件系统的工作原理、如何实现自定义事件和监听器,以及如何在实际项目中应......
  • Java循环创建对象内存溢出怎么解决
    在Java中,如果在循环中不当地创建大量对象而不及时释放内存,很容易导致内存溢出(OutOfMemoryError)。这通常发生在以下几种情况中:(1)循环内不断创建对象但对象引用未被释放:对象被创建后,如果它们一直被引用(即使是间接的),垃圾收集器(GC)就无法回收它们占用的内存。(2)循环次数过多或对象体积......