事件循环详解

事件循环的主要概念

事件循环是 js 的调度机制,所有的脚本,事件,渲染代码(任务)都需要通过事件循环来安排执行。

循环中的任务分为宏任务微任务两种队列,主要的循环逻辑是:

  1. 先从宏任务队列中取出一个任务处理
  2. 然后执行整个微任务队列,如果其间有产生了微任务,继续执行,直到微任务队列为空。
  3. 回到阶段1,如此循环往复,定期轮询。

宏任务, 微任务 队列

  • 微任务队列:
    • nextTickQueue 队列:process.nextTick
    • microTaskQueue 队列:Promise, MutationObserver
    • 注意⚠️:执行时nextTickQueue 队列先清空后,再清空microTaskQueue 队列。
    • 注意⚠️:MutationObserver事件类似setInterval,添加到队列的同类任务没被处理之前不会再次插入。
  • 宏任务队列:
    • 除上述微任务之外的都是宏任务:"事件、用户交互、脚本、渲染、网络等"。
    • 注意⚠️:页面渲染也是宏任务,特殊的是渲染引擎与JS引擎是互斥的,调度的时候还多了一个切换引擎的成本。

node中分阶段的宏任务

浏览器中的宏任务没有阶段的区分,只会按照回调函数进入事件队列的顺序进行执行,node则会按照不同类型的回调函数在不同阶段有区别地执行。

node 的分阶段轮询机制

  • 每个操作阶段:

    1. 都会先执行这个阶段的特定操作。
    2. 然后执行其FIFO的队列中任务(每个阶段都有一个FIFO的队列, 直到队列为空或者达到最大数量的执行限制【每次循环估计有最大的时间限制】)。
    3. 最后移动到下一阶段。
  • 主要轮询阶段按顺序如下:

    1. timers阶段:执行到期的 setTimeoutsetInterval
    2. pending callbacks阶段:执行已经积压的I/O事件。
    3. poll阶段(⚠️此阶段有一段时间的同步阻塞 ):
      1. 计算阻塞时间
      2. 从系统内核检索新的I/O事件添加到队列,并执行(执行除了close callback, timers, setImmediate事件之外的几乎所有事件)
      3. 猜测:会将close callback, timers, setImmediate事件分配到对应的队列(所以timers阶段setTimeout嵌套不会连续执行,因为子setTimeout还没插入队列)。
      4. 如果队列为空:
        1. 如果有setImmediate,进入下一check阶段
        2. 如果没有setImmediateclose callback事件,且有timmer到期,将其添加到timmers,执行timmers队列的事件(并不是回退到timmers阶段)。
    4. check阶段: 执行setImmediate
    5. close callback:执行close事件,如socket.on('close', ...)
  • 源代码如下:

    js 复制代码
      /**
       * 可以看出,setTimeout绑定的回调函数有可能会在两个阶段被推到主线程中执行。
       * 但是因一个在前,一个在尾部,其实也可以认为又开启了一次新的轮询。
       */
      while (r != 0 && loop->stop_flag == 0) {
        // first timers
        uv__update_time(loop);
        uv__run_timers(loop);
        ran_pending = uv__run_pending(loop);
        uv__run_idle(loop);
        uv__run_prepare(loop);
    
        timeout = 0;
        if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) {
          timeout = uv_backend_timeout(loop);
        }
    
        uv__io_poll(loop, timeout);
        uv__run_check(loop);
        uv__run_closing_handles(loop);
    
        if (mode == UV_RUN_ONCE) {
        // second timers
          uv__update_time(loop);
          uv__run_timers(loop);
        }
    
        r = uv__loop_alive(loop);
        if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
          break;
      }

实际应用

  • setImmediate() 为什么比 setTimeout(callback, 0) 快?

    • 因为虽然setTimeout/setInterval的时间设置为0,但是实际上系统会将其设置为1
    • poll阶段处理函数中的setImmediate肯定比setTimeout/setInterval早执行。因为下一阶段就是check阶段。
    js 复制代码
      /**
        * 输出顺序:
        * setImmediate
        * setTimeout
        * 
        * *这说明timmers执行期间添加的timer并不会像微任务队列那样接着执行,*
        * 也可能到期后需要其他阶段来辅助添加到队列,如poll阶段才会分配到队列,
        * 因此导致setImmediate总是比setTimeout先执行。
        */
      setTimeout(() => {
        setTimeout(() => {
          console.log('setTimeout');
        },0);
    
        setImmediate(() => {
          console.log('setImmediate');
        });
        // 中断10ms
        const now = new Date().getTime();
        while (true) {
        if (new Date().getTime() - now > 100) {
          break;
        } 
        }
      }, 100);
  • setTimeout/setInterval

    • 时间设置为0,但是系统会将其设置为1ms。如果嵌套超过4层,则系统会将其设置为4ms。
    • 可以使用setImmediate代替setTimeout
  • postMessage延迟比setTimeout(callback, 0)更快的原因应该是poll阶段只是分配了setTimeout(callback, 0)但没执行,而postMessage的事件被执行了。


参考

"为了协调事件、用户交互、脚本、渲染、网络等,用户代理必须使用本节中描述的事件循环"--《HTML Standard》

Nodejs

node定时器详解

How the event loop work·

相关推荐
神夜大侠19 分钟前
VUE 实现公告无缝循环滚动
前端·javascript·vue.js
明辉光焱21 分钟前
【Electron】Electron Forge如何支持Element plus?
前端·javascript·vue.js·electron·node.js
柯南二号1 小时前
HarmonyOS ArkTS 下拉列表组件
前端·javascript·数据库·harmonyos·arkts
究极无敌暴龙战神X1 小时前
前端学习之ES6+
开发语言·javascript·ecmascript
明辉光焱1 小时前
【ES6】ES6中,如何实现桥接模式?
前端·javascript·es6·桥接模式
nameofworld2 小时前
前端面试笔试(二)
前端·javascript·面试·学习方法·数组去重
hummhumm2 小时前
第 12 章 - Go语言 方法
java·开发语言·javascript·后端·python·sql·golang
hummhumm2 小时前
第 8 章 - Go语言 数组与切片
java·开发语言·javascript·python·sql·golang·database
zhanghaisong_20153 小时前
Caused by: org.attoparser.ParseException:
前端·javascript·html·thymeleaf
南城夏季4 小时前
蓝领招聘二期笔记
前端·javascript·笔记