当前位置:首页 > 技术 > 正文内容

深度解析 Promise 链式调用中四种 then 传参方式的执行时序差异

访客 技术 2026年10月22日 1

在 JavaScript 异步编程中,Promise.then() 的参数传递方式看似微小,却会引发截然不同的执行逻辑与时序结果。本文通过重构一道经典 Promise 执行顺序题,系统性地揭示四种常见写法背后的微任务调度机制、值穿透行为及返回值处理规则。

核心代码重构(语义等价,结构优化)

const taskA = () => Promise.resolve(1);
const taskB = () => Promise.resolve(2);

// Case 1: 显式返回 Promise
taskA()
  .then(() => taskB())
  .then(val => console.log('case-1:', val));

// Case 2: 执行但不返回
taskA()
  .then(() => {
    taskB(); // 调用但忽略返回值
  })
  .then(val => console.log('case-2:', val));

// Case 3: 传入函数调用结果(非函数)
taskA()
  .then(taskB()) // ❌ 传入的是 Promise 实例,非函数引用
  .then(val => console.log('case-3:', val));

// Case 4: 传入函数引用(正确简写)
taskA()
  .then(taskB) // ✅ 等效于 .then((x) => taskB(x))
  .then(val => console.log('case-4:', val));

关键机制解析

1. 微任务嵌套层级决定执行延迟

每个 .then() 注册本身是同步操作,但其回调执行被推入微任务队列。当回调内返回一个 Promise 时,该 Promise 的 resolve 过程会触发新一轮微任务调度:

  • taskA().then(() => taskB()):外层 .then 创建第1次微任务;taskB() 返回的 Promise 在 resolve 后再创建第2次微任务;最终 .then(val => ...) 的执行需等待前两次微任务完成 → 共3层延迟。
  • 若回调仅返回普通值(如 return 42),则仅触发第1次微任务(Promise.resolve(42) 的同步 resolve 不产生新微任务,仅排队后续 .then)→ 2层延迟。

2. 返回值类型影响链式行为

.then() 回调的返回值决定下一级 Promise 的状态与值:

返回值类型下一级 .then 接收值微任务次数
undefined / number / stringPromise.resolve(value) 包装后的值2
Promise 实例该 Promise resolve 后的值3(含 Promise 内部 resolve 延迟)
无显式 return(隐式 undefined)undefined2

3. 值穿透(Value Propagation)规则

当 .then() 的参数不是函数时,Promise 规范要求进行"值穿透"——即跳过当前环节,将上游 resolved 值直接透传至下一个有效处理器:

// 下列写法均触发穿透:
taskA().then(null).then(val => console.log(val));        // 输出 1
taskA().then(42).then(val => console.log(val));          // 输出 1
taskA().then(taskB()).then(val => console.log(val));     // 输出 1(taskB() 是 Promise,非函数!)

注意:taskB() 是函数调用表达式,返回 Promise 对象,而非函数本身。因此 .then(taskB()) 中的参数不符合函数类型要求,触发穿透。

4. 函数引用 vs 函数调用的本质区别

.then(fnRef) 与 .then(fnCall()) 存在根本差异:

  • fnRef 是函数引用,由 Promise 在状态变更后自动调用,接收上游值作为参数;
  • fnCall() 是立即执行的函数调用,其返回值(如 Promise)被传给 .then,而 Promise 会检测该值是否为函数 —— 若非函数则穿透。

因此 .then(taskB) 等效于 .then((val) => taskB(val)),而 .then(taskB()) 等效于 .then(Promise.resolve(2)),后者因参数非法触发穿透。

执行时序推演

基于上述规则,四组调用的微任务注册与执行顺序如下:

  1. Case 2:无返回 → undefined 被包装为 Promise.resolve(undefined) → 2次微任务 → 最先输出 case-2: undefined
  2. Case 3:taskB() 非函数 → 值穿透 → taskA() 的结果 1 直接透传 → 2次微任务 → 次之输出 case-3: 1
  3. Case 1:显式返回 Promise → 3次微任务 → 第三输出 case-1: 2
  4. Case 4:函数引用 → 等效于 Case 1 → 同样 3次微任务,但注册时间略晚于 Case 1 → 最后输出 case-4: 2

最终控制台输出顺序为:

case-2: undefined
case-3: 1
case-1: 2
case-4: 2

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。