深度解析 Promise 链式调用中四种 then 传参方式的执行时序差异
在 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 / string | Promise.resolve(value) 包装后的值 | 2 |
Promise 实例 | 该 Promise resolve 后的值 | 3(含 Promise 内部 resolve 延迟) |
无显式 return(隐式 undefined) | undefined | 2 |
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)),后者因参数非法触发穿透。
执行时序推演
基于上述规则,四组调用的微任务注册与执行顺序如下:
- Case 2:无返回 →
undefined被包装为Promise.resolve(undefined)→ 2次微任务 → 最先输出case-2: undefined - Case 3:
taskB()非函数 → 值穿透 →taskA()的结果1直接透传 → 2次微任务 → 次之输出case-3: 1 - Case 1:显式返回 Promise → 3次微任务 → 第三输出
case-1: 2 - Case 4:函数引用 → 等效于 Case 1 → 同样 3次微任务,但注册时间略晚于 Case 1 → 最后输出
case-4: 2
最终控制台输出顺序为:
case-2: undefined
case-3: 1
case-1: 2
case-4: 2