返回专题

专题 2021

JS 执行机制

- NodeJS 有主执行线程, I/O线程, worker_threads 线程, 没有定时器线程, 也没有宏任务栈的概念, 但是在 NodeJS 11 的某个版本之后, 主动向浏览器的逻辑靠拢, 导致可以近似理解为 NodeJS 是有宏任务栈的

JavaScript个人笔记必读系列
本文目录7
  1. 浏览器和 NodeJS 执行机制
  2. macroTaskQueue/microTaskQueue/nextTickQueue 执行顺序
  3. 宏任务微任务兼容
  4. 微任务基本原理
  5. 宏任务基本原理
  6. 特殊示例
  7. Reference
  • 目录

    浏览器和 NodeJS 执行机制

    macroTaskQueue/microTaskQueue/nextTickQueue 执行顺序

    宏任务微任务兼容

    微任务基本原理

    宏任务基本原理

    特殊示例

    Reference

浏览器和 NodeJS 执行机制

  • 浏览器执行机制图

  • NodeJS 执行机制图

    libuv七队列图解

    阶段概述:

    每个框被称为事件循环机制的一个阶段, 每个阶段都有一个 FIFO 队列来执行回调

    • **定时器:**本阶段执行已经被 setTimeout()setInterval() 的调度回调函数。
    • **待定回调:**执行延迟到下一个循环迭代的 I/O 回调。
    • idle, prepare:仅系统内部使用。
    • **轮询:**检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
    • 检测:setImmediate() 回调函数在这里执行。
    • **关闭的回调函数:**一些关闭的回调函数,如:socket.on('close', ...)

macroTaskQueue/microTaskQueue/nextTickQueue 执行顺序

  • 浏览器环境> [!important]

    setImmediate 不是浏览器标准功能, 兼容性不好

    • 宏任务: setTimeout, setInterval, I/O,setImmediate(如果存在, 目前只有 Edge 和 IE11 支持),requestAnimationFrame(存在争议)
    • 微任务: Promises,MutationObserver

    ==总结:==

    • ==浏览器环境下, 会依次执行== **宏任务 -> 微任务栈 -> 宏任务 -> 微任务栈**
  • NodeJS 环境> [!important]

    NodeJS 有主执行线程, I/O线程, worker_threads 线程, 没有定时器线程, 也没有宏任务栈的概念, 但是在 NodeJS 11 的某个版本之后, 主动向浏览器的逻辑靠拢, 导致可以近似理解为 NodeJS 是有宏任务栈的

    • **阶段任务:**setTimeout, setInterval, setImmediate
    • **微任务:**process.nextTick, promise.then

    ==总结:==

    • ==NodeJS 11 的某个版本之前: 在每个== timmer ==和== immediate ==执行之后, 并不会执行== nextTick栈 → 微任务栈==, 而是会在每个阶段结束前统一清空==
    • ==NodeJS 11 的某个版本之后: 在每个== timmer ==和== immediate ==执行之后, 都会执行== nextTick栈 → 微任务栈==, 为了与浏览器事件机制保持一致==
      • 复现代码

        JavaScript
        setTimeout(() => {
          console.log('timer1');
          Promise.resolve().then(function() {
            console.log('promise1');
          });
        }, 0);
        setTimeout(() => {
          console.log('timer2');
          Promise.resolve().then(function() {
            console.log('promise2');
          });
        }, 0);
        
        // 这里加长主任务运行时间, 保证 setTimeout 回调时间已到
        let old = Date.now();
        while (Date.now() - old <= 1000) {}
        
        // 在浏览器环境和 NodeJS 11 的某个版本之后的执行结果为 timer1, promise1, timer2, promise2
        // 在 NodeJS 11 的某个版本之前, 结果为 timer1, timer2, promise1, promise2
        

    相关 Issue:

    1. timers: run nextTicks after each immediate and timer #22842.
    2. MacroTask and MicroTask execution order #22257.
    3. implement queueMicrotask #22951.

宏任务微任务兼容

微任务基本原理

核心微任务代码实现见 core-js 的 microtask 模块

  1. 优先使用 queueMicrotask 方法
  2. 浏览器可以使用 MutationObserver, 除了 iOS 环境
  3. 有些环境实现了非完全正确的 Promise(例如 WebKit ~ iOS Safari 10.1), 可以使用 Promise.then 实现
  4. NodeJS 使用 process.nextTick
  5. 其他环境使用 macrotask 实现

宏任务基本原理

核心微任务代码实现见 core-js 的 task 模块

  1. 优先使用 setImmediateclearImmediate
  2. 早期 NodeJS 0.8 - 没有 immediate, 使用 process.nextTick
  3. Sphere (JS game engine) 使用 Dispatch.now
  4. 浏览器使用 MessageChannel 除了 iOS 环境
  5. 其他浏览器(除了 IE8 -), 使用 global.postMessageglobal.addEventListener('message', ...)
  6. 其他浏览器使用 html.appendChild(createElement('script'))['onreadystatechange']
  7. 其余的 setTimeout 兜底

特殊示例

  • 关于 setTimeout/setInterval 0 延迟> [!important]

    The HTML5 standard says: “after five nested timers, the interval is forced to be at least 4 milliseconds.”

    结果说明:

    • 在浏览器中如果 setTimeout 被嵌套了 5 次, 之后的回调间隔至少为 4ms
    • 在 NodeJS 中执行没有这个限制

    原因分析:

    • 如果没有延迟有可能会导致 UI 锁死, CPU 过载
    • 操作系统的 clock 本身也不是很准确

    相关代码:

    • setTimeout 示例代码

      JavaScript
      let start = Date.now();
      let times = [];
      
      setTimeout(function run() {
        times.push(Date.now() - start); 
        // remember delay from the previous call
        if (start + 100 < Date.now()) {
          console.log(times);
        } else {
          // show the delays after 100ms
          setTimeout(run);
        }
      });
      
      // an example of the output:
      // 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100ÏÏ
      
    • setInterval 示例代码

      JavaScript
      let start = Date.now();
      let times = [];
      
      const timmerId = setInterval(function run() {
        times.push(Date.now() - start); 
        // remember delay from the previous call
        if (start + 100 < Date.now()) {
          clearInterval(timmerId);
          console.log(times);
        }
      });
      
  • 关于 process.nextTick 和 Promise.then

    • process.nextTick 比 Promise.then 先执行

      JavaScript
      process.nextTick(() => {
        console.log(1);
        process.nextTick(() => {
          console.log(2);
        });
      });
      
      new Promise((resolve) => {
        console.log(3);
        resolve();
      }).then(() => {
        console.log(4);
      });
      
      process.nextTick(() => {
        console.log(5);
      });
      
      // 输出 3,1,5,2,4
      
    • process.nextTick 注册的回调会在事件循环的当前阶段结束前执行

      JavaScript
      /**
       * 执行栈中注册 setTimeout 计时器
       */
      setTimeout(function () {
        // 4. timers 阶段。timer watcher 遍历计时器 Map 的 key,
        // 如果有 key <= timeout,执行该 key 对应的 value(计时器任务);
        // 否则等到 poll 阶段再检查一次
        console.log("setTimeout");
      
        setTimeout(function () {
          // 11. 注册 setTimeout 计时器。UV_RUN_ONCE 模式下,
          // 会在循环结束之前再执行时间下限到达的计时器任务,取决于进程性能
          // 1 <= TIMEOUT_MAX <= 2 ^ 31 - 1
          console.log("setTimeout in setTimeout");
        }, 0);
      
        setImmediate(function () {
          // 9. 注册 setImmediate 计时器。在当前循环的 check 阶段执行。
          // (注:这是新的 ImmediateList,当前循环内有 3 个 ImmediateList 了)
          console.log("setImmediate in setTimeout");
        });
      
        process.nextTick(function () {
          // 6. 为 nextTickQueue 添加任务,timers 阶段结束前唤醒 idle watcher
          // idle watcher 检查 nextTickQueue,执行任务
          console.log("nextTick in setTimeout");
        });
      }, 0);
      
      /**
       * 执行栈中注册 setImmediate 计时器
       */
      setImmediate(function () {
        // 7. poll 阶段没有可执行任务,阶段结束前唤醒 idle watcher,idle watcher 继续睡;
        // 接着唤醒 check watcher,检测到 ImmediateList 不为空,进入 check 阶段。
        // check watcher 执行第一个任务
        console.log("setImmediate");
      
        setTimeout(function () {
          // 13. 注册 setTimeout 计时器
          // 由于机器性能,在循环结束前才执行
          console.log("setTimeout in setImmediate");
        }, 0);
      
        setImmediate(function () {
          // 12. 为当前 ImmediateList 添加任务
          // 由于机器性能优越,前面 nextTickQueue 为空了,直接进入 check 阶段
          console.log("setImmediate in setImmediate");
        });
      
        process.nextTick(function () {
          // 10. 为 nextTickQueue 添加任务,当所有 ImmediateList 的队首任务都执行完毕时,
          // 唤醒 idle watcher,检查 nextTickQueue,执行队列任务
          console.log("nextTick in setImmediate");
        });
      });
      
      /**
       * 执行栈中为 nextTickQueue 添加任务
       */
      process.nextTick(function () {
        // 2. 执行栈为空,进入事件循环准备阶段,唤醒 prepare watcher,
        // 检查 nextTickQueue,执行队列中的任务
        console.log("nextTick");
      
        setTimeout(function () {
          // 5. 注册计时器任务,timers 阶段到达时间下限则执行该任务,
          // 否则等到 poll 阶段
          console.log("setTimeout in nextTick");
        }, 0);
      
        setImmediate(function () {
          // 8. 注册 setImmediate 计时器,在当前循环的 check 阶段执行。
          // (注:这是新的 ImmediateList,当前循环内有 2 个 ImmediateList 了)
          console.log("setImmediate in nextTick");
        });
      
        process.nextTick(function () {
          // 3. prepare watcher 处于活跃状态,检测 nextTickQueue 的新任务,
          // 执行完所有任务后沉睡
          console.log("nextTick in nextTick");
        });
      });
      
      console.log("main thread"); // 1. 执行栈的任务
      
  • 关于 Promise.then 和 Promise.resolve 执行顺序

    • 示例1

      JavaScript
      Promise.resolve()
        .then(() => {
          // mt1
          console.log(0);
          // 以下语法同 Promise.resolve()
          return new Promise(resolve => {
            // 内部为 mt8
            resolve();
          });
        })
        .then(() => {
          // mt2
          console.log(4);
        });
      
      Promise.resolve()
        .then(() => {
          // mt3
          console.log(1);
        })
        .then(() => {
          // mt4
          console.log(2);
        })
        .then(() => {
          // mt5
          console.log(3);
        })
        .then(() => {
          // mt6
          console.log(5);
        });
      
      /**
       * 1. Stack: null; nextTick: mt1, mt3 => none
       * 2. Stack: mt1, mt3; nextTick: null => none
       * 3. Stack: mt3; nextTick: mt8 => 0
       * 4. Stack: null; nextTick: nextTick(mt8), mt4 => 1
       * 5. Stack: mt4; nextTick: mt8 => none
       * 6. Stack: null; nextTick: mt8, mt5 => 2
       * 7. Stack: mt8, mt5; nextTick: null => none
       * 8. Stack: mt5; nextTick: mt2 => none
       * 9. Stack: null; nextTick: mt2, mt6 => 3
       * 10. Stack: mt2, mt6; nextTick: null => none
       * 11. Stack: mt6; nextTick: null => 4
       * 12. Stack: null; nextTick: mt7 => 5
       * 13. Stack: mt7; nextTick: null => none
       * 14. Stack: null; nextTick: null => 6
       */
      
    • 示例2

Reference

破阵九解:Node和浏览器之事件循环/任务队列/异步顺序/数据结构

本文内容比较长,请见谅。如有评议,还请评论区指点,谢谢大家! >> 目录 开门见山:Node和浏览器的异步执行顺序问题 两种环境下的宏任务和微任务(macrotask && microtask) Node和浏览器的事件循环模型在实现层面的区别 Node和浏览器的事件循环的任务队列(task queue) Node和浏览器的事件循环模型在表现层面的差异 理清libuv的"7队列"和Node"6队列"的关系 Node和浏览器环境下setTimeout的最小延迟时间 setTimeout和setImmediate的执行顺序详解 Node相关组成结构中涉及的数据结构 >> Node端的异步执行顺序 Node端的异步执行顺序如下 同步代码 > process.
https://zhuanlan.zhihu.com/p/86530853

当事件循环遇到更新渲染

说到js的执行机制,相信大家都可以说出来: js引擎在执行过程中,遇到一个异步事件后并不会一直等待其返回结果,而是将其挂起(等异步任务返回结果,就会添加到事件队列中),继续执行执行栈中的任务; 每次执行栈完毕后,立刻去处理微任务队列; 微任务队列处理完成后,再去检查事件队列是否有任务,如果有就继续执行.
https://zhuanlan.zhihu.com/p/267273074

David Baron's weblog: setTimeout with a shorter delay

On Sunday, somebody with the nickname {g} was on irc.
https://dbaron.org/log/20100309-faster-timeouts

Node.js 事件循环,定时器和 process.nextTick() | Node.js

在 GitHub 上编辑 事件循环是 Node.
https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/

又被node的eventloop坑了,这次是node的锅

近日在论坛上看到一篇文章讲node和谷歌浏览器的eventloop的区别,因为看到写的还不错,我表示了肯定。但没过多久一位坛友却说node11结果不一样,我说怎么可能不一样。接着坛友贴了个代码,我试着运行了一下,啪啪打脸! 先上被啪啪打脸的代码: setTimeout(() => { console.
https://zhuanlan.zhihu.com/p/54951550

图解浏览器的基本工作原理

可能每一个前端工程师都想要理解浏览器的工作原理。 我们希望知道从在浏览器地址栏中输入 url 到页面展现的短短几秒内浏览器究竟做了什么; 我们希望了解平时常常听说的各种代码优化方案是究竟为什么能起到优化的作用; 我们希望更细化的了解浏览器的渲染流程。 一个好的程序常常被划分为几个相互独立又彼此配合的模块,浏览器也是如此,以 Chrome 为例,它由多个进程组成,每个进程都有自己核心的职责,它们相互配合完成浏览器的整体功能,每个进程中又包含多个线程,一个进程内的多个线程也会协同工作,配合完成所在进程的职责。 对一些前端开发同学来说,进程和线程的概念可能会有些模糊,为了更好的理解浏览器的多进程架构,这里我们简单讨论一下进程和线程。 进程(process)和线程(thread) 当我们启动一个应用,计算机会创建一个进程,操作系统会为进程分配一部分内存,应用的所有状态都会保存在这块内存中,应用也许还会创建多个线程来辅助工作,这些线程可以共享这部分内存中的数据。如果应用关闭,进程会被终结,操作系统会释放相关内存。更生动的示意图如下: 一个进程还可以要求操作系统生成另一个进程来执行不同的任务,系统会为新的进程分配独立的内存,两个进程之间可以使用 IPC (Inter Process Communication)进行通信。很多应用都会采用这样的设计,如果一个工作进程反应迟钝,重启这个进程不会影响应用其它进程的工作。 如果对进程及线程的理解还存在疑惑,可以参考下述文章。 进程与线程的一个简单解释 - 阮一峰的网络日志 浏览器的架构 有了上面的知识做铺垫,我们可以更合理的讨论浏览器的架构了,其实如果要开发一个浏览器,它可以是单进程多线程的应用,也可以是使用 IPC 通信的多进程应用。 不同浏览器采用了不同的架构模式,这里并不存在标准,本文以 Chrome 为例进行说明 : Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。 具体说来,Chrome 的主要进程及其职责如下: 负责包括地址栏,书签栏,前进后退按钮等部分的工作; 负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问; 负责一个 tab 内关于网页呈现的所有事情 负责控制一个网页用到的所有插件,如 flash Chrome 多进程架构的优缺点 优点 某一渲染进程出问题不会影响其他进程 更为安全,在系统层面上限定了不同进程的权限 缺点 负责处理
https://zhuanlan.zhihu.com/p/47407398

  • 面试前读物:EventLoop为何这么设计?
返回首页
上一篇多端低代码编辑器设想下一篇TS

Discussion

留言与讨论

想法、补充和不同意见都欢迎。