事件循环详解

事件循环的主要概念

事件循环是 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·

相关推荐
惜.己34 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
长天一色1 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
Vue3 Pinia持久化存储
开发语言·javascript·ecmascript
读心悦2 小时前
如何在 Axios 中封装事件中心EventEmitter
javascript·http
神之王楠2 小时前
如何通过js加载css和html
javascript·css·html
余生H2 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
流烟默3 小时前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
茶卡盐佑星_3 小时前
meta标签作用/SEO优化
前端·javascript·html
与衫3 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
金灰3 小时前
HTML5--裸体回顾
java·开发语言·前端·javascript·html·html5