系列文章目录

《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)



前言

用户可见的 JavaScript 主线程在任一时刻通常只执行一段同步代码;定时器、Promise、DOM 等「稍后执行」的逻辑依赖宿主引擎协作的调度模型。理解调用栈宏任务微任务怎样交替消费,是阅读输出顺序题、安排异步流程(含 async/await)以及解释框架中 nextTick 等行为的基础。本篇先建立执行栈画面,再归结事件循环中的一轮处理,最后给出可对照运行的示例与易错点。


一、单线程、调用栈与「先跑完当前同步代码」

  1. 单线程(主线):日常所说的「JS 是单线程」多指负责 DOM 与用户交互的主线程上的 JS 执行模型;Web Worker 等可在独立线程运行脚本,但与主线程通过消息通信,不能直接操作 DOM,此处不展开。
  2. 调用栈:函数调用形成栈帧的压入与弹出;当前栈上的代码连续执行,直到栈空或让出(例如 await 之后的续体由引擎另行调度)。
  3. 同步块不可分割:一段同步代码执行期间,不会被「插入」另一个宏任务回调去打断;计时器到期Promise 决议等只会在合适的时机把回调放入队列,待当前同步工作告一段落后再处理。

由此得到直觉:先顺序执行完当前同步代码(以及随后必须立刻清掉的微任务),再轮到计时器等宏任务。


二、执行上下文(与作用域的衔接)

每次进入函数、模块或全局脚本,都会创建对应的执行上下文(概念层可理解为:词法环境、变量环境、对外层引用、以及 this 绑定等信息的载体)。执行上下文的进栈与出栈伴随调用栈变化,与第 2 篇中的词法作用域、作用域链描述的是同一套运行时的不同切面:前者偏「谁在上层、谁在栈里」,后者偏「标识符解析沿链查找」。本篇侧重何时执行、谁先谁后;提升、块级作用域等仍以第 2 篇为准。


三、事件循环:一轮里大致发生什么

「事件循环」是引擎与宿主之间的协作约定,不同宿主细节略有差异,教学上可采用下列简化模型

  1. 宏任务队列取出一个任务执行(整个脚本的首次执行也可视为一个宏任务单元)。
  2. 该宏任务对应的同步代码跑完后,依次清空微任务队列(若执行微任务时又产生新微任务,会继续排到当前轮末尾,直到微任务队列为空)。
  3. 必要时由浏览器插入渲染等步骤(与刷新率、requestAnimationFrame 相关,后文略提)。
  4. 再取下一个宏任务,重复上述过程。

要点:微任务优先级高于「尚未开始的下一个宏任务」;同一轮里可以连续执行很多个微任务。


四、宏任务与微任务:常见成员

宏任务多由宿主调度入队,例如:

  • setTimeoutsetInterval(到期后回调作为宏任务)
  • I/O、部分网络回调(环境与实现相关)
  • UI 渲染相关调度(表述上归在浏览器事件循环模型中,不必死记与某个 API 一一绑定)

微任务多由引擎维护,例如:

  • Promise.prototype.then / catch / finally 的回调
  • queueMicrotask 显式入队的任务
  • MutationObserver 的回调在浏览器中常以微任务形式调度

注意:「哪些是宏 / 微」不必背诵穷尽列表,核心是排队规则一轮内的先后顺序


五、推导「输出顺序」的固定步骤

遇到面试输出题,可按下面顺序心算:

  1. 标出所有同步 console.log 的执行顺序。
  2. 标出本轮结束后将入队的微任务(含 Promise.thenqueueMicrotask 等)。
  3. 标出将入队的宏任务(如 setTimeout 回调)。
  4. 同步结束后:先清空全部微任务(含微任务里新长的微任务),执行下一个宏任务。

下面是与常用讲义一致的经典例子(输出在注释中):

console.log("1"); // 同步

setTimeout(() => console.log("2"), 0); // 宏任务

Promise.resolve().then(() => console.log("3")); // 微任务

queueMicrotask(() => console.log("4")); // 微任务

console.log("5"); // 同步

// 输出顺序:1, 5, 3, 4, 2

含义简述:1、5 为同步;本轮宏任务(脚本)尾巴上顺次执行微任务 3、42 排在之后的宏任务 turn。


六、Promise 构造器与 async/await

  1. Promise 构造器形参executor)在创建 Promise同步调用;其中 resolve()/reject() 会决议 Promise,但 .then 注册的回调仍为微任务,在同步段之后执行。
  2. async 函数:调用后返回 Promise;函数体在第一个 await 之前的代码同步执行。
  3. await 右侧:若为 Thenable,其后的代码相当于放进微任务链(与「拆成 .then」的教学模型一致);因此会出现「先打印同步的 D,再进入 await 之后的逻辑」的现象。
const delay = (ms) => new Promise((r) => setTimeout(r, ms));

async function run() {
  console.log("A"); // 同步(进入 run 后立即执行)

  await delay(100);
  // 从续体开始可视为微任务(delay 完成后触发)
  console.log("B");

  await Promise.resolve();
  console.log("C"); // 微任务
}

run();
console.log("D"); // 同步

// 输出顺序:A, D, B, C(B、C 之间无其它输出时)

setTimeout(fn, 0):并不代表「立即插队到当前同步代码中间」;最短延迟受环境限制(浏览器里常见为约 4ms 下限等实现细节,Node 里计时器行为另有规范,面试题通常不抠具体毫秒,只需知道晚于本轮同步与微任务)。


七、requestAnimationFrame 与渲染

requestAnimationFrame 的回调安排在下一次重绘前,语义上服务于动画与读布局,不宜与「宏任务 / 微任务」做简单等同(部分教材写「既不是宏也不是微」即提醒勿硬套)。实践中要记住:它不适合替代精确定时器;与 setTimeout 混排时顺序以环境实现为准,题目中不如 Promise + setTimeout 常见。


八、工程上为何关心微任务

  1. 输出顺序与 Bug 复现:同一轮里多次 thensetTimeout 交错,顺序错了就难以排查。
  2. 批量更新:如 Vue 2 的 $nextTick、Vue 3 的内部调度,常依靠微任务把多次数据变更合并到一次刷新前后处理,避免不必要的中间渲染。
  3. 异步控制流Promise 链与 async/await 本质都建立在决议与微任务之上,后续专门讨论 Promise 时可以再展开状态机细节。

九、易混淆点归纳

  1. 只有 .then / .catch / .finally 回调才是微任务new Promise(() => { ... }) 里写的代码多半是同步的。
  2. async 函数从头执行到第一个 await(不含续体)仍是同步
  3. 微任务可以嵌套产生微任务:会一直 drain 到空,才可能进入下一个宏任务;死循环入队微任务会卡死主线程。
  4. instanceof、闭包、this 与本篇正交:执行顺序题要在对调用栈与队列有把握的前提下再叠加上去。
  5. 跨环境表述:浏览器与 Node 在宏任务细分(如 process.nextTick 在 Node 中具有更前位置)上不完全一致,遇到 Node 题需单独查当前版本约定。

十、思考与练习

1. 下面代码输出什么?

Promise.resolve().then(() => console.log("p1"));
console.log("sync");
Promise.resolve().then(() => console.log("p2"));

解析:sync 先同步输出;随后在同一轮清空微任务:p1p2。结果为 sync, p1, p2

2. 为何「微任务太晚堆积」可能导致页面卡顿?

解析:微任务在渲染等步骤之前会被连续清空;若单轮内微任务过多,会推迟浏览器响应其它工作与视觉更新,主观上表现为卡顿。

3. setTimeout(0)Promise.resolve().then(...) 谁先执行?

解析:当前同步代码结束后,执行微任务里的 then轮到计时器宏任务(在典型浏览器模型下)。


总结

  • 主线程上同步代码执行;调用栈执行上下文描述「如何跑完这一段」。
  • 事件循环一轮内:跑一个宏任务清空微任务队列 → 再取下一宏任务(简化模型)。
  • Promise.thenqueueMicrotask 等为微任务setTimeout回调多为宏任务;这是输出题的支点。
  • async/await 的续体可按微任务理解await 之前仍有同步段。

下一篇计划整理 数组常用算法与手写练习(与大纲中「常用算法」部分衔接;防抖、节流等也可在后续专题中成文)。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐