深入理解 JavaScript Promise 异步执行顺序与微任务队列

深入理解 JavaScript Promise 异步执行顺序与微任务队列

本文深入探讨 javascript 中 promise 异步函数的执行机制,特别是 `then` 方法如何与微任务队列(promisejob queue)协同工作。通过一个具体代码示例,我们将逐步解析代码执行流程、promise 状态变化以及回调函数入队与出队的时机,揭示 `console.log` 输出顺序背后的原理,帮助开发者掌握 promise 异步行为的精确控制。

JavaScript 异步编程基础与 Promise 机制

JavaScript 是一种单线程语言,这意味着它一次只能执行一个任务。为了处理耗时的操作(如网络请求、文件读写),JavaScript 引入了异步编程机制。Promise 是 ES6 引入的一种处理异步操作的模式,它代表一个异步操作的最终完成(或失败)及其结果值。

理解 Promise 的执行顺序,关键在于把握 JavaScript 的事件循环(Event Loop)、调用(Call Stack)、微任务队列(Microtask Queue,也称 PromiseJob Queue)和宏任务队列(Macrotask Queue)的概念。

调用栈 (Call Stack):同步代码执行的地方。当函数被调用时,它被推入栈中;函数返回时,它被弹出。微任务队列 (Microtask Queue):存放 Promise 的 then/catch/finally 回调函数等。微任务在当前宏任务执行完毕后,且在下一个宏任务开始前,会被清空。宏任务队列 (Macrotask Queue):存放如 setTimeout、setInterval、I/O 操作等回调。在清空微任务队列后,事件循环会从宏任务队列中取出一个任务执行。

Promise 的核心特性:

Promise.resolve():创建一个已解决(fulfilled)的 Promise,这个操作本身是同步的。then() 方法:当一个 Promise 被解决(fulfilled)时,其 .then() 中注册的回调函数会被放入微任务队列。then() 方法本身总是返回一个新的 Promise,这个新 Promise 的状态最初是 pending。它的解决或拒绝取决于 then 回调函数的执行结果。即使 then() 是在一个已解决的 Promise 上调用的,它返回的 Promise 仍然是 pending,因为其回调需要异步执行。

示例代码分析

为了更清晰地演示 Promise 的执行顺序,我们使用一个带有命名 Promise 和回调函数的示例代码:

立即学习“Java免费学习笔记(深入)”;

var a = Promise.resolve(); // Promise a 立即解决var b = a.then(function a_then() {  console.log(1);  var c = Promise.resolve(); // Promise c 立即解决  var d = c.then(function c_then() {    console.log(2);  });  var e = d.then(function d_then() {    console.log(3);  });  console.log(4);});var f = b.then(function b_then() {  console.log(5);  var g = Promise.resolve(); // Promise g 立即解决  var h = g.then(function g_then() {    console.log(6);  });  var i = h.then(function h_then() {    console.log(7);  });  console.log(8);});console.log(9);

我们将逐行分析这段代码的执行流程,并观察 console.log 的输出顺序。

详细执行流程解析

以下是代码执行的详细步骤,关注调用栈、Promise 状态和微任务队列的变化:

阶段一:同步代码执行

var a = Promise.resolve();

a 被创建并立即进入 fulfilled 状态。a 的状态: fulfilled。微任务队列: 空。

var b = a.then(function a_then() { … });

a.then() 被调用。由于 a 已经 fulfilled,a_then 回调函数被添加到微任务队列。b 被创建,状态为 pending。a 的状态: fulfilled;b 的状态: pending。微任务队列: [a_then]。

var f = b.then(function b_then() { … });

b.then() 被调用。由于 b 仍是 pending 状态,b_then 回调函数在 b 解决后才会被添加。f 被创建,状态为 pending。a 的状态: fulfilled;b 的状态: pending;f 的状态: pending。微任务队列: [a_then]。

console.log(9);

同步代码执行,输出 9。输出: 9微任务队列: [a_then]。

至此,所有同步代码执行完毕。调用栈清空,事件循环开始检查微任务队列。

阶段二:第一次微任务队列处理 (a_then)

事件循环从微任务队列中取出 a_then 并执行。

微任务队列: 空。

console.log(1); (在 a_then 内部)

输出 1。输出: 9, 1

var c = Promise.resolve();

c 被创建并立即进入 fulfilled 状态。c 的状态: fulfilled。

var d = c.then(function c_then() { … });

c.then() 被调用。由于 c 已经 fulfilled,c_then 回调函数被添加到微任务队列。d 被创建,状态为 pending。微任务队列: [c_then]。

var e = d.then(function d_then() { … });

d.then() 被调用。由于 d 仍是 pending 状态,d_then 回调函数在 d 解决后才会被添加。e 被创建,状态为 pending。微任务队列: [c_then]。

console.log(4); (在 a_then 内部)

输出 4。输出: 9, 1, 4

a_then 函数执行完毕,其返回值(undefined)用于解决 b。

b 的状态变为 fulfilled。由于 b 已经 fulfilled,之前注册在 b.then() 上的 b_then 回调函数现在被添加到微任务队列。微任务队列: [c_then, b_then]。

阶段三:第二次微任务队列处理 (c_then)

事件循环从微任务队列中取出 c_then 并执行。

微任务队列: [b_then]。

console.log(2); (在 c_then 内部)

输出 2。输出: 9, 1, 4, 2

c_then 函数执行完毕,其返回值(undefined)用于解决 d。

d 的状态变为 fulfilled。由于 d 已经 fulfilled,之前注册在 d.then() 上的 d_then 回调函数现在被添加到微任务队列。微任务队列: [b_then, d_then]。

阶段四:第三次微任务队列处理 (b_then)

事件循环从微任务队列中取出 b_then 并执行。

微任务队列: [d_then]。

console.log(5); (在 b_then 内部)

输出 5。输出: 9, 1, 4, 2, 5

var g = Promise.resolve();

g 被创建并立即进入 fulfilled 状态。g 的状态: fulfilled。

var h = g.then(function g_then() { … });

g.then() 被调用。由于 g 已经 fulfilled,g_then 回调函数被添加到微任务队列。h 被创建,状态为 pending。微任务队列: [d_then, g_then]。

var i = h.then(function h_then() { … });

h.then() 被调用。由于 h 仍是 pending 状态,h_then 回调函数在 h 解决后才会被添加。i 被创建,状态为 pending。微任务队列: [d_then, g_then]。

console.log(8); (在 b_then 内部)

输出 8。输出: 9, 1, 4, 2, 5, 8

b_then 函数执行完毕,其返回值(undefined)用于解决 f。

f 的状态变为 fulfilled。微任务队列: [d_then, g_then]。

阶段五:第四次微任务队列处理 (d_then)

事件循环从微任务队列中取出 d_then 并执行。

微任务队列: [g_then]。

console.log(3); (在 d_then 内部)

输出 3。输出: 9, 1, 4, 2, 5, 8, 3

d_then 函数执行完毕,其返回值(undefined)用于解决 e。

e 的状态变为 fulfilled。微任务队列: [g_then]。

阶段六:第五次微任务队列处理 (g_then)

事件循环从微任务队列中取出 g_then 并执行。

微任务队列: 空。

console.log(6); (在 g_then 内部)

输出 6。输出: 9, 1, 4, 2, 5, 8, 3, 6

g_then 函数执行完毕,其返回值(undefined)用于解决 h。

h 的状态变为 fulfilled。由于 h 已经 fulfilled,之前注册在 h.then() 上的 h_then 回调函数现在被添加到微任务队列。微任务队列: [h_then]。

阶段七:第六次微任务队列处理 (h_then)

事件循环从微任务队列中取出 h_then 并执行。

微任务队列: 空。

console.log(7); (在 h_then 内部)

输出 7。输出: 9, 1, 4, 2, 5, 8, 3, 6, 7

h_then 函数执行完毕,其返回值(undefined)用于解决 i。

i 的状态变为 fulfilled。微任务队列: 空。

最终输出顺序为:9, 1, 4, 2, 5, 8, 3, 6, 7。

注意事项与总结

同步优先原则:JavaScript 总是优先执行所有同步代码。只有当调用栈清空后,事件循环才会检查微任务队列。微任务队列的优先级:微任务队列在每个宏任务(包括主脚本的执行)结束后立即清空。这意味着在一个宏任务内部产生的微任务,会在当前宏任务结束,但下一个宏任务开始之前被执行。then() 返回的新 Promise 总是 pending:理解 then() 方法返回的 Promise 总是从 pending 状态开始非常重要。它的最终状态取决于其回调函数的执行结果,而回调函数是异步执行的。Promise 链式调用:当一个 Promise 解决后,其 .then() 回调会被加入微任务队列。如果这个回调内部又返回了一个 Promise,那么下一个 .then() 的回调会等待这个内部 Promise 解决后才会被加入微任务队列。Promise.resolve() 的同步性:Promise.resolve() 本身是同步的,它会立即创建一个已解决的 Promise。但是,对这个 Promise 调用 .then() 所注册的回调,仍然是异步的,会被放入微任务队列。

通过以上详细的步骤分析,我们可以清晰地看到 JavaScript Promise 异步机制的内部工作原理,特别是微任务队列在调度 Promise 回调中的核心作用。掌握这些概念对于编写健壮、可预测的异步 JavaScript 代码至关重要。

以上就是深入理解 JavaScript Promise 异步执行顺序与微任务队列的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1539917.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月21日 12:01:17
下一篇 2025年12月21日 12:01:25

相关推荐

  • JavaScript 中从对象数组中提取并优化唯一键值对

    本教程详细介绍了如何在JavaScript中处理一个包含多个对象的数组,并从中移除重复的键值对。通过构建一个高效的算法,利用 `reduce` 和一个 `seen` 映射来跟踪已出现的键值组合,最终生成一个仅包含唯一键值对的新对象数组,从而实现数据清洗和优化。 在处理复杂的数据结构时,我们经常会遇到…

    2025年12月21日
    000
  • 如何在React递归函数中条件性地停止执行

    本文探讨了在React路径查找应用中,如何有效地条件性停止递归函数执行。针对`useState`在递归场景下异步更新导致停止逻辑失效的问题,文章提出了通过直接检查目标元素(如路径终点)的`visited`状态来替代组件状态变量,并优化了递归调用的停止条件、状态更新方式及代码可读性,确保递归函数能够即…

    2025年12月21日
    000
  • 从CSS文件提取自定义字体font-weight的JavaScript教程

    本教程详细介绍了如何使用javascript的`cssstylesheet` api,从用户上传的自定义css文件中高效、准确地解析并提取所有`@font-face`规则中定义的`font-weight`值。通过动态创建`cssstylesheet`并遍历其`cssrules`,我们可以识别字体规则…

    2025年12月21日
    000
  • 解决 JavaScript fetch 请求重复触发问题:循环内异步调用的陷阱

    本文深入探讨了 javascript `fetch` 请求意外多次触发的常见问题,这通常导致后端重复处理请求并可能引发网络错误。文章揭示了问题的根源在于将异步 `fetch` 函数的定义与调用不当地放置在循环内部。通过详细的案例分析和代码重构,教程展示了如何将 `fetch` 操作移至循环外部,确保…

    2025年12月21日
    000
  • 深入解析与解决React Context中的无限循环问题

    本文旨在深入探讨React Context组件中因不当状态管理和副作用处理导致的无限循环问题。我们将分析在组件渲染阶段直接调用setState与useEffect依赖项结合如何触发循环,并提供一个健壮的解决方案,通过将初始状态同步逻辑移至useEffect钩子,有效防止不必要的重渲染,确保应用性能与…

    2025年12月21日
    000
  • 理解JavaScript中函数立即执行与闭包对返回类型的影响

    本文深入探讨了javascript中函数立即执行表达式(iife)的工作原理及其对变量赋值和返回类型的影响。通过分析一个常见示例,我们揭示了外部函数立即执行后,变量f被赋值为内部函数,从而导致后续调用f()时实际执行的是内部函数并返回一个数字,而非外部函数本身。文章同时阐述了闭包如何在此过程中保持内…

    2025年12月21日
    000
  • Photoshop脚本:智能检测参考线并执行自动化操作

    本文将详细介绍如何使用adobe photoshop脚本(extendscript)来检测当前活动文档中是否存在参考线。我们将提供一个实用的脚本示例,该脚本能够根据参考线的存在与否,智能地执行不同的自动化操作,例如当没有参考线时自动全选画布,或者在有参考线时执行用户自定义的动作。文章将深入解析关键代…

    2025年12月21日
    000
  • Chart.js进阶:通过自定义插件控制图表与图例布局间距

    本文旨在解决chart.js中图表与图例之间间距调整的常见难题。由于chart.js默认配置无法直接实现这一特定间距的精确控制,文章将深入探讨如何通过创建并集成一个自定义插件来修改图例的布局行为。我们将详细介绍插件的编写原理、配置方法,并提供完整的示例代码,帮助开发者灵活调整图表布局,实现更精细的视…

    2025年12月21日
    000
  • 避免Chrome浏览器阻止JavaScript生成的空ZIP文件下载

    本文探讨了在使用JavaScript客户端生成ZIP文件时,Chrome浏览器可能阻止下载的问题。核心发现是,Chrome会将空的ZIP文件标记为潜在危险并阻止下载。教程将指导开发者识别并解决因ZIP文件内容为空导致的下载阻塞,确保文件包含有效数据,从而实现顺畅的客户端下载体验。 理解Chrome阻…

    2025年12月21日
    000
  • 如何隐藏HTML input type=”date” 的默认占位符

    本教程详细介绍了如何通过CSS有效隐藏HTML input type=”date” 元素中默认显示的“dd/mm/yyyy”占位符。针对标准CSS属性无法直接控制其内部渲染的问题,文章提出利用Webkit浏览器特有的伪元素,如 ::-webkit-datetime-edit-…

    2025年12月21日
    000
  • JavaScript基础计算器中小数点输入与计算的优化实践

    本教程旨在解决javascript基础计算器应用中,小数点输入后消失或导致计算错误的问题。通过优化数字和运算符的输入处理逻辑,确保小数点能够正确显示和参与计算,避免将2.5错误地解析为25。核心策略在于精确管理显示字段的字符串值与内部数值变量的转换时机,从而实现稳定可靠的小数点运算功能。 1. 问题…

    2025年12月21日
    000
  • JavaScript中精准定位元素进行动画处理的实践指南

    本教程详细阐述了如何在javascript中精确选择特定html元素(如`div`内的图片)进行动画处理,避免影响页面上其他无关元素。文章通过`getelementsbyclassname`、`getelementsbytagname`和`queryselectorall`等多种dom选择器,结合示…

    2025年12月21日 好文分享
    000
  • Webpack打包TypeScript类到全局作用域的策略与实践

    本文深入探讨了在Webpack中将TypeScript编译并打包为JavaScript文件后,如何有效地将其中定义的类暴露给外部JavaScript环境。文章详细介绍了通过`output.library`配置实现模块命名空间化(如UMD)和直接全局暴露两种主要方法,并提供了相应的Webpack配置示…

    2025年12月21日
    000
  • 从自定义CSS字体文件中提取font-weight的JavaScript教程

    本教程详细介绍了如何使用javascript的`cssstylesheet` api从用户上传的自定义css字体文件中动态解析并提取`@font-face`规则中的`font-weight`、`font-family`和`font-style`信息。这对于构建字体选择器或需要根据css内容动态显示可…

    2025年12月21日
    000
  • JavaScript中高效清空DOM元素:优化“删除全部”功能

    本文探讨了在javascript中实现“删除全部”dom元素功能时,如何避免常见的for循环陷阱,并提供了两种更高效、更可靠的方法:利用innerhtml = “”快速清空,以及结合queryselectorall和foreach迭代删除。通过代码示例和最佳实践,帮助开发者优…

    2025年12月21日
    000
  • React Hook Form: 高效处理空字符串字段为 Null 的策略

    本文探讨了在React Hook Form中将提交数据中的空字符串字段转换为`null`的有效策略。针对循环调用`setValue`可能遇到的问题,文章提供了一种直接在`onSubmit`回调中转换数据对象的方法,确保数据在发送到API前符合预期格式,并兼顾了代码的清晰性和效率。 问题背景:Reac…

    2025年12月21日
    000
  • 理解JavaScript中立即执行函数与闭包的返回值类型

    本文深入探讨了JavaScript中立即执行函数表达式(IIFE)与闭包的结合如何影响函数返回值的类型。通过分析一个常见代码示例,我们揭示了外部函数被立即调用后,其返回的内部闭包函数被赋值给变量,导致后续调用该变量时,实际执行的是内部函数并返回其结果(通常是数字),而非函数本身,从而澄清了类型判断的…

    2025年12月21日
    000
  • JavaScript插件开发_javascript扩展功能

    开发JavaScript插件需先明确目标与使用场景,如增强DOM操作或适配特定框架(Vue/jQuery),设计简洁API并提供默认配置,支持模块化引入,保证兼容性与健壮性,检测全局对象与参数类型,编写清晰文档和示例,便于集成与维护。 开发JavaScript插件,核心是封装可复用的功能,使其能被轻…

    2025年12月21日
    000
  • 解决React应用中地图组件生产环境不渲染问题:Browserslist配置优化

    本教程旨在解决React应用中地图组件(如基于Maplibre GL或Leaflet)在开发环境正常、生产环境却无法渲染的问题。通过分析常见的`Uncaught ReferenceError`错误,我们发现核心症结在于构建过程中的JavaScript兼容性。文章将详细指导如何通过优化`package…

    2025年12月21日
    000
  • 在 Vue 3 vue-i18n 中深度访问翻译对象及实现方法

    本文旨在解决 Vue 3 中 `vue-i18n` 无法直接通过父级键访问嵌套翻译对象的问题。针对 `legacy: false` 模式下 `$t(‘parent’)` 返回键名而非完整对象的情况,文章将详细介绍如何通过创建自定义的 `$td`(translate deep)…

    2025年12月21日
    000

发表回复

登录后才能评论
关注微信