js主线程执行流程

419 阅读22分钟

执行流程

    1[主线程] 
    23├─ 执行同步代码 ->
    4│              |
    5│              v
    6│    → 遇到异步操作(如 setTimeout)  → 交给宿主环境线程处理         「微任务队列一个   
    7│                                     │         
    8│                                     ├─ 定时器线程(等待时间到达   不同宏任务队列 」                  
    9│                                     ├─ 网络线程(处理请求)         
   10│                                     └─ ...
   11└─ 事件循环 → 轮询任务队列 → 按规则执行回调

问:帮我解释下js的异步更新,详细解释下,如果是多个异步同时进行,如何做的,这些异步事件可能的先后顺序和什么有关系

答:JavaScript 的异步更新机制是单线程事件循环模型的核心内容,其执行顺序由 事件循环(Event Loop)任务队列(Task Queue) 共同控制。以下是详细解释:

一、异步执行机制基础

  1. 单线程模型
    JavaScript 是单线程的,所有任务(同步/异步)都在主线程中执行。异步操作通过 非阻塞 I/O事件驱动 机制实现并行效果。

  2. 事件循环流程

    复制
    1主线程 → 执行同步代码 → 遇到异步任务 → 交给 Web API 处理 → Web API 完成后将回调推入任务队列 → 事件循环持续检查队列 → 队列中的回调按规则进入主线程执行。
    

二、异步任务的类型

JavaScript 中异步任务分为两种优先级:

  1. 宏任务(Macrotask)

    • 包括:setTimeoutsetIntervalI/O 操作UI renderingscript 标签代码
    • 执行规则:每个事件循环周期执行一个宏任务。
  2. 微任务(Microtask)

    • 包括:Promise.then()MutationObserverqueueMicrotask
    • 执行规则:在 当前宏任务结束后、下一个宏任务开始前 清空所有微任务队列。

三、多个异步任务的执行顺序

当多个异步任务同时触发时,执行顺序由以下因素决定:

1. 任务类型优先级
复制
1// 示例代码
2setTimeout(() => console.log('宏任务1'), 0);
3Promise.resolve().then(() => console.log('微任务1'));
4setTimeout(() => console.log('宏任务2'), 0);
5Promise.resolve().then(() => console.log('微任务2'));
6
7// 输出顺序:
8// 微任务1 → 微任务2 → 宏任务1 → 宏任务2
  • 微任务优先于宏任务:所有微任务会在下一个宏任务前执行完毕。
2. 任务触发时机
  • 相同类型任务按注册顺序执行
    例如多个 Promise.then() 按调用顺序执行。
  • 定时器误差
    setTimeout(fn, 0) 的实际延迟可能大于 0(受浏览器最小延迟限制,通常 4ms)。
3. I/O 操作完成时间
复制
1fs.readFile('fileA', () => console.log('File A'));
2fs.readFile('fileB', () => console.log('File B'));
3// 输出顺序取决于文件读取速度

四、事件循环的阶段性处理

在浏览器和 Node.js 中,事件循环分为多个阶段(以 Node.js 为例):

  1. Timers 阶段:执行 setTimeout 和 setInterval 回调。
  2. I/O Callbacks 阶段:执行 I/O 回调(如文件读写)。
  3. Idle/Prepare 阶段:内部使用。
  4. Poll 阶段:检索新的 I/O 事件。
  5. Check 阶段:执行 setImmediate 回调。
  6. Close 阶段:执行关闭事件的回调(如 socket.on('close'))。

五、控制异步顺序的实践

  1. 使用 Promise 链式调用

    复制
    1Promise.resolve()
    2  .then(() => task1())
    3  .then(() => task2());
    
  2. async/await 同步化写法

    复制
    1async function run() {
    2  await task1();
    3  await task2();
    4}
    
  3. 并行执行 + 等待所有完成

    复制
    1Promise.all([task1(), task2()]).then(() => {
    2  console.log('All done!');
    3});
    

六、总结表格

影响因素说明
任务类型(宏/微)微任务优先于宏任务
任务注册顺序同类型任务按调用顺序执行
外部资源响应速度如网络请求、文件读取的完成时间
定时器精度浏览器/Node.js 的最小延迟限制
事件循环阶段划分Node.js 中不同阶段处理不同类型任务

通过理解这些规则,可以更精准地预测和控制异步代码的执行顺序。

除了宏任务和微任务还有什么任务?

在 JavaScript 的异步模型中,除了常见的 宏任务(Macrotask)微任务(Microtask) ,还存在一些 特殊任务类型环境相关的调度机制,它们的执行顺序和优先级有所不同。以下是详细分类和说明:

一、核心任务类型(基础)

任务类型示例执行优先级
宏任务setTimeoutsetInterval, I/O 操作, UI 渲染, 事件回调每轮事件循环处理一个宏任务
微任务Promise.then/catch/finallyMutationObserverqueueMicrotask每轮宏任务结束后清空所有微任务

二、特殊任务类型(补充)

1. Node.js 特有任务
任务类型示例执行顺序
process.nextTickprocess.nextTick(callback)优先级高于微任务,在事件循环阶段切换时立即执行(比微任务更早)
setImmediatesetImmediate(callback)在 Node.js 的 轮询(Poll)阶段之后 执行,属于一种特殊的宏任务

示例代码:

复制
1setTimeout(() => console.log('宏任务'), 0);
2Promise.resolve().then(() => console.log('微任务'));
3process.nextTick(() => console.log('nextTick'));
4setImmediate(() => console.log('setImmediate'));
5
6// 输出顺序:
7// nextTick → 微任务 → setImmediate → 宏任务
2. 浏览器特有任务
任务类型示例执行时机
requestAnimationFramerequestAnimationFrame(callback)在浏览器 下一次重绘前执行,属于高优先级宏任务
requestIdleCallbackrequestIdleCallback(callback)在浏览器 空闲时段执行,属于低优先级宏任务

示例代码:

复制
1setTimeout(() => console.log('普通宏任务'), 0);
2requestAnimationFrame(() => console.log('RAF 宏任务'));
3Promise.resolve().then(() => console.log('微任务'));
4
5// 输出顺序:
6// 微任务 → RAF 宏任务 → 普通宏任务

三、任务优先级总结

在不同环境中,任务类型的执行顺序按优先级从高到低排列:

浏览器环境
  1. 同步代码
  2. 微任务PromiseMutationObserver
  3. requestAnimationFrame(特殊宏任务)
  4. 普通宏任务setTimeout, 事件回调)
  5. requestIdleCallback(空闲时执行)
Node.js 环境
  1. 同步代码
  2. process.nextTick
  3. 微任务Promise
  4. setImmediate(特殊宏任务)
  5. 普通宏任务setTimeout, I/O 回调)

四、混合任务场景分析

复制
1// 浏览器示例
2setTimeout(() => console.log('宏任务1'), 0);
3requestAnimationFrame(() => console.log('RAF'));
4Promise.resolve().then(() => console.log('微任务'));
5setTimeout(() => console.log('宏任务2'), 0);
6
7// 输出顺序:
8// 微任务 → RAF → 宏任务1 → 宏任务2
复制
1// Node.js 示例
2setTimeout(() => console.log('setTimeout'), 0);
3setImmediate(() => console.log('setImmediate'));
4process.nextTick(() => console.log('nextTick'));
5Promise.resolve().then(() => console.log('微任务'));
6
7// 输出顺序:
8// nextTick → 微任务 → setTimeout → setImmediate
9//(注意:setTimeout 和 setImmediate 顺序可能互换,取决于事件循环启动时间)

五、其他注意事项

  1. queueMicrotask
    浏览器和 Node.js 均支持,用于显式添加微任务。

    复制
    1queueMicrotask(() => console.log('显式微任务'));
    
  2. 任务饥饿问题
    如果微任务或 process.nextTick 无限递归添加任务,会阻塞事件循环。

  3. Web Workers
    在 Worker 线程中,任务模型与主线程一致,但独立运行。

六、总结表格

任务类型环境优先级典型场景
process.nextTickNode.js高于微任务确保回调在事件循环继续前完成
setImmediateNode.js普通宏任务之后推迟到当前阶段完成
requestAnimationFrame浏览器普通宏任务之前动画渲染优化
requestIdleCallback浏览器最低优先级执行非紧急任务(如日志上报)

理解这些任务的执行顺序,能帮助优化代码性能,避免异步逻辑的时序错误。

它所有的事件都在一个主线程执行是吧,只不过通过这些任务来分先后执行顺序?

是的,你的理解完全正确!JavaScript 的核心设计是 单线程主线程执行所有任务,通过 事件循环(Event Loop)任务队列(Task Queue) 的机制来调度异步任务的执行顺序,从而模拟“非阻塞”行为。以下详细解释这一机制:

一、为什么单线程能处理异步?

JavaScript 运行在单线程中,但通过 任务队列 + 事件循环 实现异步非阻塞。核心流程:

  1. 主线程:执行同步代码(如函数调用、变量赋值)。
  2. 异步任务:遇到异步操作(如 setTimeoutfetch)时,将任务交给 宿主环境(浏览器/Node.js)的其他线程处理(如定时器线程、网络线程)。
  3. 回调入队:异步操作完成后,回调函数被推入任务队列
  4. 事件循环:主线程空闲时,从队列中取出任务执行。

二、主线程与任务队列的关系

1. 主线程的职责
  • 执行所有 同步代码(包括函数调用、表达式计算)。
  • 处理 任务队列中的回调(按事件循环规则)。
2. 任务队列的类型
  • 宏任务队列:存放 setTimeoutsetInterval、DOM 事件、I/O 回调等。
  • 微任务队列:存放 Promise.thenMutationObserverprocess.nextTick(Node.js)等。
3. 事件循环流程
复制
1主线程执行流程:
21. 执行当前宏任务(如 script 脚本整体)。
32. 清空所有微任务队列。
43. 渲染(浏览器环境下:样式计算、布局、绘制)。
54. 从宏任务队列中取下一个任务,重复步骤 1~4。

三、单线程异步的示例分析

代码示例:
复制
1console.log("同步1");
2
3setTimeout(() => {
4  console.log("宏任务1");
5  Promise.resolve().then(() => console.log("微任务1"));
6}, 0);
7
8Promise.resolve().then(() => console.log("微任务2"));
9
10console.log("同步2");
执行过程:
  1. 同步代码执行

    • 输出 同步1 → 同步2
    • 异步操作 setTimeout 和 Promise.then 被交给宿主环境处理。
  2. 微任务优先

    • 当前宏任务(整体 script)结束,清空微任务队列 → 输出 微任务2
  3. 渲染阶段(浏览器可能在此处更新 UI)。

  4. 下一个宏任务

    • 执行 setTimeout 回调 → 输出 宏任务1
    • 回调中的 Promise.then 产生微任务 → 立即清空 → 输出 微任务1

四、单线程异步的优缺点

优点
  • 避免多线程竞争:无需处理锁、死锁等并发问题。
  • 逻辑简单可控:代码按顺序执行,时序可预测。
缺点
  • 阻塞风险:长时间运行的同步代码会阻塞后续任务(如复杂计算、死循环)。

    复制
    1// 同步代码阻塞示例
    2console.log("开始");
    3for (let i = 0; i < 1e10; i++) {} // 模拟耗时操作
    4setTimeout(() => console.log("被阻塞的任务"), 0);
    5console.log("结束");
    
    • 输出顺序:开始 → (长时间等待) → 结束 → 被阻塞的任务

五、如何优化单线程性能?

  1. 拆分长任务

    • 使用 setTimeout 或 requestIdleCallback 分割任务。
    复制
    1function chunkTask() {
    2  doPart(); // 执行一部分任务
    3  if (hasMore) setTimeout(chunkTask, 0); // 让出主线程
    4}
    
  2. Web Workers

    • 将计算密集型任务交给独立线程(如数据处理、图像分析)。
    复制
    1// 主线程
    2const worker = new Worker("worker.js");
    3worker.postMessage(data);
    4worker.onmessage = (e) => { /* 处理结果 */ };
    5
    6// worker.js
    7self.onmessage = (e) => {
    8  const result = heavyCompute(e.data);
    9  self.postMessage(result);
    10};
    
  3. 合理使用微任务

    • 微任务适合需要尽快执行的逻辑(如状态更新),但避免微任务嵌套导致饥饿。

六、总结:单线程的执行模型

特性说明
单线程所有代码在主线程执行,无并行代码执行
非阻塞异步依赖宿主环境的多线程能力处理 I/O、定时器等,回调通过队列通知主线程
任务优先级微任务 > 宏任务 > 渲染(浏览器)
性能瓶颈长时间同步任务会阻塞后续所有操作
优化手段任务拆分、Web Workers、合理使用异步 API

附:主线程与宿主环境协作图

复制
1[主线程] 
23├─ 执行同步代码 → 遇到异步操作(如 setTimeout) → 交给宿主环境线程处理
4│                                               │
5│                                               ├─ 定时器线程(等待时间到达)
6│                                               ├─ 网络线程(处理请求)
7│                                               └─ ...
89└─ 事件循环 → 轮询任务队列 → 按规则执行回调

那意思它在主线程就是通过看任务队列里的事件,然后循环,所以叫事件循环? 事件循环到底怎么定义的,就是区分出来同步,宏任务,微任务?

是的,JavaScript 的 事件循环(Event Loop) 本质是一个持续运行的循环机制,它的核心作用是 监控任务队列,并按照特定规则从队列中取出任务交给主线程执行。它的名称来源于“循环检查队列中的事件”这一行为。以下是它的核心定义和工作原理:


一、事件循环的定义

事件循环是 JavaScript 运行时(如浏览器、Node.js)处理异步任务的机制,它通过以下步骤不断循环:

  1. 检查任务队列(是否有待执行的任务)。
  2. 取出任务(按优先级规则)。
  3. 执行任务(在主线程中运行)。
  4. 重复循环,直到所有队列为空。

二、事件循环的分层规则

事件循环的核心是 任务优先级管理,它将任务分为三类:

任务类型执行规则
同步任务在主线程中立即执行,阻塞后续代码。
宏任务在每一轮事件循环中执行 一个,例如 setTimeoutDOM 事件I/O 回调
微任务在 当前宏任务执行完毕后、下一个宏任务开始前 清空整个微任务队列。

三、事件循环的完整流程

以浏览器环境为例,事件循环的具体步骤如下:

  1. 执行当前宏任务

    • 运行同步代码(例如脚本初始化逻辑)。
    • 遇到异步操作(如 setTimeoutPromise),将回调注册到对应的队列中。
  2. 清空微任务队列

    • 执行所有微任务(例如 Promise.thenMutationObserver)。
    • 如果微任务中又产生新的微任务,继续清空,直到队列为空。
  3. 渲染页面(浏览器特有)

    • 执行 requestAnimationFrame 回调。
    • 计算样式(Style)、布局(Layout)、绘制(Paint)。
  4. 取下一个宏任务

    • 从宏任务队列中取出一个任务(如 setTimeout 回调、点击事件)。
    • 重复步骤 1~4。

四、代码示例理解执行顺序

复制
1console.log("同步任务 1");
2
3setTimeout(() => console.log("宏任务 1"), 0);
4setTimeout(() => console.log("宏任务 2"), 0);
5
6Promise.resolve().then(() => {
7  console.log("微任务 1");
8  Promise.resolve().then(() => console.log("嵌套微任务"));
9});
10
11console.log("同步任务 2");

输出顺序

复制同步任务 1
同步任务 2
微任务 1
嵌套微任务
宏任务 1
宏任务 2

执行过程

  1. 执行同步代码,输出 同步任务 1同步任务 2
  2. 清空微任务队列:输出 微任务 1,嵌套的微任务 嵌套微任务 也会被立即执行。
  3. 执行下一个宏任务(setTimeout 回调),按顺序输出 宏任务 1宏任务 2

五、不同环境的差异

1. 浏览器环境
  • 宏任务包括:setTimeoutDOM 事件requestAnimationFrame(特殊宏任务)、I/O 回调
  • 微任务包括:Promise.thenMutationObserver
2. Node.js 环境
  • 宏任务分为多个阶段:timers(定时器)、I/O callbacksidlepoll(轮询)、checksetImmediate)。
  • 微任务包括:Promise.thenprocess.nextTick(优先级最高)。

六、事件循环的比喻

可以将事件循环想象成一个 餐厅服务员

  1. 同步任务:顾客直接点餐,服务员立即处理。
  2. 宏任务:顾客要求打包外卖,服务员记下订单,等当前顾客处理完再去处理外卖。
  3. 微任务:顾客在结账时要求加一杯饮料,服务员必须立即处理完,才能服务下一个顾客。

七、总结

  • 事件循环:通过循环检查队列,决定主线程执行哪个任务的机制。

  • 任务分类

    • 同步任务:立即执行。
    • 宏任务:按队列顺序,每一轮循环执行一个。
    • 微任务:在每轮循环的末尾清空整个队列。
  • 核心作用:在单线程中实现非阻塞的异步行为,保证高响应性。

遇到异步操作(如 setTimeout、Promise),将回调注册到对应的队列中。这句话什么意思?队列还有好几个了?

是的,JavaScript 的事件循环中存在 多个任务队列,不同的异步任务会被分发到对应的队列中,并按特定规则执行。下面详细解释其机制:


一、为什么会有多个队列?

JavaScript 为了高效处理异步任务,将不同类型的回调分为 宏任务(Macrotask)微任务(Microtask) ,分别放入不同的队列。这样做有两大目的:

  1. 优先级控制:微任务优先级更高,确保某些任务(如 Promise 状态变更)能及时处理。
  2. 任务分类管理:区分轻重任务,避免耗时操作阻塞主线程。

二、队列的分类及对应任务

1. 宏任务队列(Macrotask Queue)
  • 任务来源setTimeoutsetInterval、I/O 操作(如文件读写)、DOM 事件回调(如点击、滚动)、requestAnimationFrame(浏览器)等。
  • 执行规则:每次事件循环只执行 一个宏任务,按入队顺序处理。
2. 微任务队列(Microtask Queue)
  • 任务来源Promise.thenPromise.catchPromise.finallyMutationObserver(浏览器)、process.nextTick(Node.js)等。
  • 执行规则:每次宏任务执行完毕后,必须 清空整个微任务队列(所有微任务一次性执行完)。
3. 同步代码(直接执行)
  • 任务来源:非异步代码(如函数调用、变量赋值、console.log)。
  • 执行规则:立即在主线程执行,无队列概念。

三、异步回调如何注册到队列?

当遇到异步操作时,JavaScript 引擎会将回调函数注册到对应的队列中。具体流程如下:

1. 示例代码
复制
1console.log("同步代码 1");
2
3// 宏任务:注册到宏任务队列
4setTimeout(() => {
5  console.log("宏任务回调");
6}, 0);
7
8// 微任务:注册到微任务队列
9Promise.resolve().then(() => {
10  console.log("微任务回调");
11});
12
13console.log("同步代码 2");
2. 执行步骤
  1. 执行同步代码

    • 输出 同步代码 1 → 同步代码 2
  2. 处理异步任务

    • setTimeout 的回调被注册到 宏任务队列
    • Promise.then 的回调被注册到 微任务队列
  3. 清空微任务队列

    • 主线程同步代码执行完毕后,立即执行所有微任务回调(输出 微任务回调)。
  4. 执行下一个宏任务

    • 从宏任务队列中取出 setTimeout 的回调执行(输出 宏任务回调)。

四、队列的底层实现

1. 宏任务队列可能有多个
  • 不同来源的宏任务可能被分配到不同队列(如定时器队列、I/O 队列、DOM 事件队列),但事件循环会按规范定义的顺序轮询这些队列。
2. 微任务队列只有一个
  • 所有微任务共享一个队列,且必须一次性清空。
3. 示例:多队列优先级
复制
1// 宏任务 1:定时器队列
2setTimeout(() => console.log("宏任务 1"));
3
4// 宏任务 2:I/O 队列(假设为 Node.js 环境)
5fs.readFile("file.txt", () => console.log("宏任务 2"));
6
7// 微任务
8Promise.resolve().then(() => console.log("微任务"));
9
10// 输出顺序:
11// 微任务 → 宏任务 1 → 宏任务 2(不同环境可能有差异)

五、关键总结

  • 多队列存在:事件循环通过多个队列分类管理任务(宏任务、微任务)。

  • 注册回调:异步操作触发时,回调会被放入对应队列。

  • 执行顺序

    1. 同步代码 → 2. 微任务队列(全部清空) → 3. 宏任务队列(每次取一个)→ 重复循环。

六、常见误区

  • 误区 1setTimeout(fn, 0) 会立即执行?
    纠正:它只是将回调放入宏任务队列,必须等待同步代码和微任务执行完毕。
  • 误区 2:微任务会阻塞渲染?
    纠正:微任务在渲染前执行,但长时间运行的微任务会导致页面卡顿。
  • 误区 3:所有异步任务都是宏任务?
    纠正Promise.then 属于微任务,requestAnimationFrame 在浏览器中属于宏任务但优先级较高。

通过这种队列机制,JavaScript 在单线程下实现了高效、有序的异步处理。

不同来源的宏任务可能被分配到不同队列(如定时器队列、I/O 队列、DOM 事件队列),但事件循环会按规范定义的顺序轮询这些队列。这个不同队列是如何具体定义顺序的?

在事件循环中,不同来源的宏任务(如定时器、I/O、DOM 事件等)的执行顺序由宿主环境(浏览器/Node.js)的实现决定,但总体遵循一定的规范逻辑。以下是不同队列的优先级规则和细节解析:


一、规范与浏览器实现

1. WHATWG 规范原则
  • 任务源(Task Sources)分类:不同任务类型(如用户交互、网络请求、定时器等)会被分配到独立的队列。
  • 队列轮询顺序:规范未明确强制规定各队列的优先级,但要求同一任务源的队列必须按先进先出(FIFO)处理
  • 浏览器自主决策:各浏览器厂商根据用户体验优化队列优先级(如用户交互任务优先执行)。
2. 浏览器中的常见顺序(非绝对)
复制
1用户交互(点击/滚动) → DOM 操作 → 网络请求 → 定时器 → 其他宏任务
  • 用户交互任务优先级最高:例如 click 事件,确保页面响应流畅。
  • 定时器任务优先级较低:如 setTimeout/setInterval 可能被延迟执行。
  • 微任务穿插执行:每个宏任务执行后,会立即清空微任务队列。

三、Node.js 的事件循环阶段

Node.js 使用 Libuv 库实现事件循环,其宏任务队列被明确划分为多个阶段,按固定顺序轮询:

阶段描述对应任务类型
Timers执行 setTimeout/setInterval 回调定时器任务
Pending I/O处理系统级 I/O 的回调(如 TCP 错误)操作系统通知的任务(如网络错误)
Idle/Prepare内部使用的准备阶段Node.js 内部任务
Poll检索新的 I/O 事件并执行相关回调文件读取、网络请求等 I/O 回调
Check执行 setImmediate 回调当前阶段结束后立即执行的任务
Close Callbacks执行关闭事件的回调(如 socket.on('close')资源清理任务(如关闭文件描述符)
Node.js 轮询顺序
复制
1Timers → Pending I/O → Idle → Poll → CheckClose → (重复)

四、代码验证浏览器队列顺序

复制
1<button id="btn">点击触发事件</button>
2<script>
3  // 宏任务 1: 定时器
4  setTimeout(() => console.log('Timeout 1'), 0);
5
6  // 宏任务 2: DOM 事件
7  document.getElementById('btn').addEventListener('click', () => {
8    console.log('Click Event');
9  });
10
11  // 宏任务 3: 触发点击事件
12  document.getElementById('btn').click();
13
14  // 宏任务 4: 另一个定时器
15  setTimeout(() => console.log('Timeout 2'), 0);
16
17  // 同步代码
18  console.log('Sync Code End');
19</script>
可能的输出结果
复制
1Sync Code End → Click Event → Timeout 1 → Timeout 2

说明

  1. 同步代码先执行。
  2. 手动触发的 click 事件(属于 用户交互任务)优先于定时器执行。
  3. 定时器任务按注册顺序执行。

五、核心结论

  1. 浏览器优先级逻辑

    • 用户交互 > 微任务 > 渲染(RAF)> 其他宏任务(如定时器)
    • 不同浏览器可能微调顺序(如 Chrome 与 Safari 存在差异)。
  2. Node.js 阶段明确

    • 严格按照 Timers → I/O → Check/Close 的固定阶段轮询。
  3. 无法绝对控制顺序

    • 开发者应避免依赖宏任务执行顺序,必要时用 微任务 或 setImmediate(Node.js)控制时序。

六、注意事项

  • 避免阻塞主线程:长时间同步代码会延迟所有宏任务执行。
  • 优先使用微任务:如 Promise/MutationObserver 在更早时机执行。
  • 实时性要求高的任务:用户交互相关代码可放在 requestAnimationFrame 中执行,与渲染周期对齐。

也就是在编辑器中哪个宏任务在前面,就先把哪个宏任务放进对应队列?

在事件循环中,宏任务的执行顺序并不仅仅取决于代码中的书写顺序(即“在编辑器中谁在前面”),而是由以下两个核心规则共同决定:


一、核心规则

  1. 队列类型优先级
    不同来源的宏任务会被分配到不同的队列,浏览器/Node.js 会按照规范定义的队列优先级依次处理这些队列。
  2. 同一队列内的任务顺序
    同一队列中的任务按任务注册的时间顺序(即代码中调用 API 的先后顺序)执行。

二、具体场景分析

场景 1:同一类型队列中的任务
复制
1// 代码中的书写顺序:
2setTimeout(() => console.log("Timeout 1"));  // 先注册
3setTimeout(() => console.log("Timeout 2"));  // 后注册
  • 逻辑:两个 setTimeout 都属于 Timers 队列(浏览器)或 Timers 阶段(Node.js)。
  • 执行顺序:按注册顺序输出 Timeout 1 → Timeout 2
场景 2:不同类型队列中的任务
复制
1// 代码中的书写顺序:
2setTimeout(() => console.log("Timeout"));      // 先注册(Timers 队列)
3document.addEventListener("click", () => {     // 后注册(User Interaction 队列)
4  console.log("Click Event");
5});
  • 逻辑setTimeout 属于低优先级的 Timers 队列,click 事件属于高优先级的 User Interaction 队列。
  • 执行顺序:即使 setTimeout 先注册,如果用户点击了页面,Click Event 会优先输出

三、关键结论

场景执行顺序决定因素
同一队列内的任务按代码中的注册顺序(“谁先写在编辑器里”)执行。
不同队列间的任务按浏览器/Node.js 定义的队列优先级执行,与代码中的书写顺序无关。
微任务(如 Promise)在每一轮宏任务执行后,会立即清空所有微任务,优先级高于任何宏任务。

四、通过代码验证

浏览器中的例子:
复制
1console.log("Start");
2
3// 先注册定时器(Timers 队列)
4setTimeout(() => console.log("Timeout"));
5
6// 后注册点击事件(User Interaction 队列)
7document.addEventListener("click", () => console.log("Click"));
8
9// 最后注册微任务(Microtask 队列)
10Promise.resolve().then(() => console.log("Promise"));
11
12console.log("End");

输出顺序(假设用户点击了页面):

复制
1StartEnd → Promise → Click → Timeout

解释

  1. 同步代码先执行(Start → End)。
  2. 微任务优先于宏任务(Promise)。
  3. 用户交互队列优先级高于定时器队列(Click → Timeout)。

五、总结

  • 代码中的书写顺序仅影响同一队列内的任务顺序。
  • 队列优先级决定了不同类型的任务谁先执行。浏览器的用户交互、动画任务优先级高于定时器,Node.js 的 Timers 阶段优先于 Poll 阶段。
  • 微任务总是插队到当前宏任务之后、下一个宏任务之前执行。