- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
本文基于 zh.javascript.info(现代 JavaScript 教程中文版)异步章节中 微任务(Microtask) 一文展开。你将理解为什么 promise 的
.then/.catch/.finally处理程序总是异步执行,掌握微任务队列(microtask queue)的调度规则,学会控制异步代码的执行顺序,并彻底弄清浏览器unhandledrejection事件触发的真实时机。读完本文,你将具备排查异步顺序问题与 promise 错误处理问题的完整能力。
一、现象:立即 resolve 的 promise,处理程序却"迟到"
promise 的处理程序.then、.catch和.finally都是异步的。即使一个 promise 立即被 resolve,.then、.catch和.finally下面的代码也会在这些处理程序之前被执行。
示例代码如下:
let promise = Promise.resolve(); promise.then(() => alert("promise done!")); alert("code finished"); // 这个 alert 先显示如果你运行它,你会首先看到code finished,然后才是promise done。
这很奇怪,因为这个 promise 肯定是一开始就完成的。
为什么.then会在之后才被触发?这是怎么回事?
要回答这个问题,需要引入本教程异步体系中的核心概念——微任务队列。
二、微任务队列(Microtask queue):PromiseJobs
异步任务需要适当的管理。为此,ECMA 标准规定了一个内部队列PromiseJobs,通常被称为"微任务队列(microtask queue)"(V8 术语)。
如规范中所述:
- 队列(queue)是先进先出的:首先进入队列的任务会首先运行。
- 只有在 JavaScript 引擎中没有其它任务在运行时,才开始执行任务队列中的任务。
或者,简单地说,当一个 promise 准备就绪时,它的.then/catch/finally处理程序就会被放入队列中:但是它们不会立即被执行。当 JavaScript 引擎执行完当前的代码,它会从队列中获取任务并执行它。
这就是为什么在上面那个示例中 "code finished" 会先显示。
上图完整展示了这一流程:promise.then(handler)调用发生后,handler被"入队(handler enqueued)",而后续的同步代码(如alert("code finished"))立即执行;只有当脚本执行完毕(script execution finished)后,队列中的处理程序才会运行(queued handler runs)。
关键规则:处理程序永远经过队列
promise 的处理程序总是会经过这个内部队列。
如果有一个包含多个.then/catch/finally的链,那么它们中的每一个都是异步执行的。也就是说,它会首先进入队列,然后在当前代码执行完成并且先前排队的处理程序都完成时才会被执行。
三、如何控制执行顺序
如果执行顺序对我们很重要该怎么办?我们怎么才能让code finished在promise done之后出现呢?
很简单,只需要像下面这样使用.then将其放入队列:
Promise.resolve() .then(() => alert("promise done!")) .then(() => alert("code finished"));现在代码就是按照预期执行的。
这里的关键思路是:不要依赖"紧随其后的同步代码",而是把需要延后执行的代码本身也放进 promise 链中,让它成为链上后续的微任务,从而保证严格的先后顺序。
四、未处理的 rejection:unhandledrejection的真正触发时机
还记得 使用 promise 进行错误处理 一章中的unhandledrejection事件吗?
现在,我们可以确切地看到 JavaScript 是如何发现未处理的 rejection 的。
如果一个 promise 的 error 未被在微任务队列的末尾进行处理,则会出现"未处理的 rejection"。
正常来说,如果我们预期可能会发生错误,我们会在 promise 链上添加.catch来处理 error:
let promise = Promise.reject(new Error("Promise Failed!")); promise.catch(err => alert('caught')); // 不会运行:error 已经被处理 window.addEventListener('unhandledrejection', event => alert(event.reason));但是如果我们忘记添加.catch,那么,微任务队列清空后,JavaScript 引擎会触发下面这事件:
let promise = Promise.reject(new Error("Promise Failed!")); // Promise Failed! window.addEventListener('unhandledrejection', event => alert(event.reason));如果我们迟一点再处理这个 error 会怎样?例如:
let promise = Promise.reject(new Error("Promise Failed!")); setTimeout(() => promise.catch(err => alert('caught')), 1000); // Error: Promise Failed! window.addEventListener('unhandledrejection', event => alert(event.reason));现在,如果我们运行上面这段代码,我们会先看到Promise Failed!,然后才是caught。
如果我们并不了解微任务队列,我们可能会想:"为什么unhandledrejection处理程序会运行?我们已经捕获(catch)并处理了 error!"
但是现在我们知道了,当微任务队列中的任务都完成时,才会生成unhandledrejection:引擎会检查 promise,如果 promise 中的任意一个出现 "rejected" 状态,unhandledrejection事件就会被触发。
在上面这个例子中,被添加到setTimeout中的.catch也会被触发。只是会在unhandledrejection事件出现之后才会被触发,所以它并没有改变什么(没有发挥作用)。
这一节揭示了一个重要的工程结论:在浏览器中,unhandledrejection不是"立即"检测错误,而是在当前微任务队列清空后统一检查。因此,通过setTimeout这类宏任务去"补救"一个 rejection,往往已经来不及阻止全局错误事件的上报;真正的做法是在 promise 链内部尽早挂上.catch。
五、微任务与宏任务:事件循环中的位置
在 事件循环:微任务和宏任务 一章中,本教程给出了更完整的事件循环算法(与规范相比仍是简化过的):
- 从宏任务队列(例如 "script")中出队并执行最早的任务。
- 执行所有微任务:当微任务队列非空时,出队并执行最早的微任务。
- 如果有变更,则将变更渲染出来。
- 如果宏任务队列为空,则休眠直到出现宏任务。
- 转到步骤 1。
安排一个新的宏任务:使用零延迟的setTimeout(f)。 安排一个新的微任务:使用queueMicrotask(f),promise 处理程序也会通过微任务队列。
每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。微任务会在执行任何其他事件处理、渲染或任何其他宏任务之前完成。这确保了微任务之间的应用程序环境基本相同(没有鼠标坐标更改,没有新的网络数据等)。
例如,看看下面这个示例:
setTimeout(() => alert("timeout")); Promise.resolve() .then(() => alert("promise")); alert("code");这里的执行顺序是:
code首先显示,因为它是常规的同步调用。promise第二个出现,因为then会通过微任务队列,并在当前代码之后执行。timeout最后显示,因为它是一个宏任务。
用一道练习验证:微任务与宏任务的完整交织
在 2-micro-macro-queue 练习 中,教程给出了一个综合性的输出顺序问题,其 解答 一步步演示了队列的进进出出:
console.log(1); setTimeout(() => console.log(2)); Promise.resolve().then(() => console.log(3)); Promise.resolve().then(() => setTimeout(() => console.log(4))); Promise.resolve().then(() => console.log(5)); setTimeout(() => console.log(6)); console.log(7);推导过程:
- 立即输出数字
1和7,因为简单的console.log调用没有使用任何队列。 - 主代码流程执行完成后,开始执行微任务队列(其中有
console.log(3); setTimeout(...4); console.log(5)),输出3和5,同时setTimeout(() => console.log(4))把console.log(4)追加到了宏任务队列尾部。 - 当微任务队列为空后,开始执行宏任务队列,输出
2、6和4。
最终输出结果为:1 7 3 5 2 6 4。
这个练习清晰地印证了本文的核心规则:微任务队列总是先于宏任务队列被清空,且微任务之间不会插入渲染或新的事件处理。
补充:async/await的幕后
在 async/await 一章中,本教程指出await是 promise 处理的另一种形式——它同样依赖微任务队列在"幕后"工作。因此,理解微任务队列,也是理解async/await暂停与恢复机制的基础:await会暂停函数执行,但不会阻塞引擎,因为其恢复逻辑正是经由微任务队列安排的。
总结
Promise 处理始终是异步的,因为所有 promise 行为都会通过内部的 "promise jobs" 队列,也被称为"微任务队列"(V8 术语)。
.then/catch/finally处理程序总是在当前代码完成后才会被调用,且链上的每个处理程序都独立进入队列、按先进先出顺序逐个执行。- 如果我们需要确保一段代码在
.then/catch/finally之后被执行,我们可以将它添加到链式调用的.then中。 unhandledrejection事件的触发时机是"微任务队列清空之后":引擎此时统一检查仍处于 rejected 状态的 promise。延迟(如通过setTimeout)添加.catch往往赶不上,无法阻止全局错误事件。- 在大多数 JavaScript 引擎中(包括浏览器和 Node.js),微任务(microtask)的概念与"事件循环(event loop)"和"宏任务(macrotasks)"紧密相关。详细的调度算法与
queueMicrotask用法见 事件循环:微任务和宏任务 一章,promise 错误的捕获与再抛出策略见 使用 promise 进行错误处理 一章。
- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
相关推荐
深入理解 JavaScript 微任务队列(Microtask Queue):Promise 异步执行与 unhandledrejection 的底层机制
深入理解 JavaScript 微任务队列(Microtask Queue):Promise 异步执行与 unhandledrejection 的底层机制 本篇
文档/教程前端Yi-VL-6B-hf:革命性多模态AI模型深度解析,视觉问答新体验
Yi VL 6B hf:革命性多模态AI模型深度解析,视觉问答新体验 在人工智能飞速发展的今天, Yi VL 6B hf 作为一款革命性的多模态AI模型,正在重
DeepSeek-V4-Pro-Base终极指南:百万上下文MoE架构深度解析
DeepSeek V4 Pro Base终极指南:百万上下文MoE架构深度解析 DeepSeek V4 Pro Base作为当前最先进的混合专家模型,以其104
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考