JS 执行机制

Last Edited Time
Feb 11, 2022 07:58 AM
date
Sep 8, 2021
slug
js-event-loop
status
Published
tags
JavaScript
个人笔记
必读系列
summary
- NodeJS 有主执行线程, I/O线程, worker_threads 线程, 没有定时器线程, 也没有宏任务栈的概念, 但是在 NodeJS 11 的某个版本之后, 主动向浏览器的逻辑靠拢, 导致可以近似理解为 NodeJS 是有宏任务栈的
type
Post
目录
notion image

浏览器和 NodeJS 执行机制

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

macroTaskQueue/microTaskQueue/nextTickQueue 执行顺序

浏览器环境
💡
setImmediate 不是浏览器标准功能, 兼容性不好
  • 宏任务: setTimeout, setInterval, I/O,setImmediate(如果存在, 目前只有 Edge 和 IE11 支持),requestAnimationFrame(存在争议)
  • 微任务: Promises,MutationObserver
总结:
  • 浏览器环境下, 会依次执行 宏任务 -> 微任务栈 -> 宏任务 -> 微任务栈
NodeJS 环境
💡
NodeJS 有主执行线程, I/O线程, worker_threads 线程, 没有定时器线程, 也没有宏任务栈的概念, 但是在 NodeJS 11 的某个版本之后, 主动向浏览器的逻辑靠拢, 导致可以近似理解为 NodeJS 是有宏任务栈的
  • 阶段任务:setTimeout, setInterval, setImmediate
  • 微任务:process.nextTick, promise.then
总结:
  • NodeJS 11 的某个版本之前: 在每个 timmerimmediate 执行之后, 并不会执行 nextTick栈 → 微任务栈, 而是会在每个阶段结束前统一清空
  • NodeJS 11 的某个版本之后: 在每个 timmerimmediate 执行之后, 都会执行 nextTick栈 → 微任务栈, 为了与浏览器事件机制保持一致
    • 复现代码
      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.
  1. MacroTask and MicroTask execution order #22257.
  1. implement queueMicrotask #22951.
 

宏任务微任务兼容

微任务基本原理

💡
核心微任务代码实现见 core-jsmicrotask 模块
  1. 优先使用 queueMicrotask 方法
  1. 浏览器可以使用 MutationObserver, 除了 iOS 环境
  1. 有些环境实现了非完全正确的 Promise(例如 WebKit ~ iOS Safari 10.1), 可以使用 Promise.then 实现
  1. NodeJS 使用 process.nextTick
  1. 其他环境使用 macrotask 实现

宏任务基本原理

💡
核心微任务代码实现见 core-jstask 模块
  1. 优先使用 setImmediateclearImmediate
  1. 早期 NodeJS 0.8 - 没有 immediate, 使用 process.nextTick
  1. Sphere (JS game engine) 使用 Dispatch.now
  1. 浏览器使用 MessageChannel 除了 iOS 环境
  1. 其他浏览器(除了 IE8 -), 使用 global.postMessageglobal.addEventListener('message', ...)
  1. 其他浏览器使用 html.appendChild(createElement('script'))['onreadystatechange']
  1. 其余的 setTimeout 兜底

特殊示例

关于 setTimeout/setInterval 0 延迟
💡
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 示例代码
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 示例代码
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 先执行
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 注册的回调会在事件循环的当前阶段结束前执行
/**
 * 执行栈中注册 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
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.nextTick > Promise.then中的函数 > setTimeOut(0) 或 setImmediate 「备注1」 Promise中的函数,无论是resolve前的还是后的,都属于"同步代码"的范围,并不是"异步代码" 「备注2」 setTimeOut(0) 或 setImmediate的执行顺序取决于具体情况,并没有确定的先后区分 Node端异步逻辑顺序实验论证 备注1: Promise接收的函数的同步问题(实验论证) 备注2: setTimeOut(0) 或 setImmediate的执行顺序问题 这个问题比较复杂,可参考下面这篇文章 >> 浏览器的异步执行顺序问题 浏览器中,涉及的异步API有:Promise, setTomeOut,setImmediate (其中setImmediate可以忽略不计,因为它只在egde和IE11才支持,没错,Chrome和火狐都是不支持的,所以当然也不建议使用) 执行顺序 Promise.then中的函数 > setTimeOut(0)
破阵九解:Node和浏览器之事件循环/任务队列/异步顺序/数据结构
Node.js 事件循环,定时器和 process.nextTick() | Node.js
在 GitHub 上编辑 事件循环是 Node.js 处理非阻塞 I/O 操作的机制--尽管 JavaScript 是单线程处理的--当有可能的时候,它们会把操作转移到系统内核中去。 既然目前大多数内核都是多线程的,它们可在后台处理多种操作。当其中的一个操作完成的时候,内核通知 Node.js 将适合的回调函数添加到 轮询 队列中等待时机执行。我们在本文后面会进行详细介绍。 当 Node.js 启动后,它会初始化事件循环,处理已提供的输入脚本(或丢入 REPL,本文不涉及到),它可能会调用一些异步的 API、调度定时器,或者调用 process.nextTick() ,然后开始处理事件循环。 下面的图表展示了事件循环操作顺序的简化概览。 ┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc.
Node.js 事件循环,定时器和 process.nextTick() | Node.js
又被node的eventloop坑了,这次是node的锅
近日在论坛上看到一篇文章讲node和谷歌浏览器的eventloop的区别,因为看到写的还不错,我表示了肯定。但没过多久一位坛友却说node11结果不一样,我说怎么可能不一样。接着坛友贴了个代码,我试着运行了一下,啪啪打脸! 先上被啪啪打脸的代码: setTimeout(() => { console.log('timer1'); Promise.resolve().then(function() { console.log('promise1'); }); }, 0); setTimeout(() => { console.log('timer2'); Promise.resolve().then(function() { console.log('promise2'); }); }, 0); 了解node的eventloop的同学应该会这样想: 理想情况下这个就是一开始将两个setTimeout放进timers的阶段。 等到时间到达后运行timer1,把promise1的Promise放入timers的下一阶段微任务队列中,同理继续运行timers的阶段,执行timer2,把promise2的Promise放入timers的下一阶段微任务队列中。 直到timers队列全部执行完,才开始运行微任务队列,也就是promise1和promise2.那么如果机器运行良好就是以下结果: timer1 timer2 promise1 promise2 node10运行结果确实是这样,是没问题的。但node11运行后居然是: timer1 promise1 timer2 promise2 挺吃惊的,但吃惊过后还是仔细去翻node的修改日志,在node 11.0 的修改日志里面发现了这个: Timers Interval timers will be rescheduled even if previous interval threw an error.
又被node的eventloop坑了,这次是node的锅
图解浏览器的基本工作原理
可能每一个前端工程师都想要理解浏览器的工作原理。 我们希望知道从在浏览器地址栏中输入 url 到页面展现的短短几秒内浏览器究竟做了什么; 我们希望了解平时常常听说的各种代码优化方案是究竟为什么能起到优化的作用; 我们希望更细化的了解浏览器的渲染流程。 一个好的程序常常被划分为几个相互独立又彼此配合的模块,浏览器也是如此,以 Chrome 为例,它由多个进程组成,每个进程都有自己核心的职责,它们相互配合完成浏览器的整体功能,每个进程中又包含多个线程,一个进程内的多个线程也会协同工作,配合完成所在进程的职责。 对一些前端开发同学来说,进程和线程的概念可能会有些模糊,为了更好的理解浏览器的多进程架构,这里我们简单讨论一下进程和线程。 进程(process)和线程(thread) 当我们启动一个应用,计算机会创建一个进程,操作系统会为进程分配一部分内存,应用的所有状态都会保存在这块内存中,应用也许还会创建多个线程来辅助工作,这些线程可以共享这部分内存中的数据。如果应用关闭,进程会被终结,操作系统会释放相关内存。更生动的示意图如下: 一个进程还可以要求操作系统生成另一个进程来执行不同的任务,系统会为新的进程分配独立的内存,两个进程之间可以使用 IPC (Inter Process Communication)进行通信。很多应用都会采用这样的设计,如果一个工作进程反应迟钝,重启这个进程不会影响应用其它进程的工作。 如果对进程及线程的理解还存在疑惑,可以参考下述文章。 进程与线程的一个简单解释 - 阮一峰的网络日志 浏览器的架构 有了上面的知识做铺垫,我们可以更合理的讨论浏览器的架构了,其实如果要开发一个浏览器,它可以是单进程多线程的应用,也可以是使用 IPC 通信的多进程应用。 不同浏览器采用了不同的架构模式,这里并不存在标准,本文以 Chrome 为例进行说明 : Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。 具体说来,Chrome 的主要进程及其职责如下: 负责包括地址栏,书签栏,前进后退按钮等部分的工作; 负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问; 负责一个 tab 内关于网页呈现的所有事情 负责控制一个网页用到的所有插件,如 flash Chrome 多进程架构的优缺点 优点 某一渲染进程出问题不会影响其他进程 更为安全,在系统层面上限定了不同进程的权限 缺点 负责处理
图解浏览器的基本工作原理
面试前读物:EventLoop为何这么设计?