Promise 异步执行机制

参考:

1. 值穿透 / 异常穿透

总结:使用 .catch 时,会默认为没有指定失败回调的 .then 添加一个失败回调。catch 所谓的"异常穿透"并不是"一次失败就触发 catch",而是一层一层传递下来。

异常穿透的前提条件:链上所有 .then 都没有指定失败回调。如果 .catch 之前的 .then 都指定了失败回调,.catch 就失去意义。

2. 由一道 bilibili 面试题引入

var date = new Date();

console.log(1, new Date() - date);

setTimeout(() => {
  console.log(2, new Date() - date);
}, 500);

Promise.resolve().then(console.log(3, new Date() - date));

while (new Date() - date < 1000) {}

console.log(4, new Date() - date);

答案:

1 0
3 1
4 1000
2 1000

时间差可能因机器性能有微小差异。

3. Event Loop 基础

执行顺序:同步任务 → 微任务(microtask)→ 宏任务(macrotask)→ 下一个宏任务 → 微任务 → ...

  • 微任务:MutationObserver、Promise.then / catch、fetch API、process.nextTick(Node)、Object.observe(已废弃)等。
  • 宏任务:script 整体、setTimeout、setInterval、setImmediate、I/O、UI rendering、postMessage、MessageChannel。

4. 由浅入深:经典题目解析

题目一:同步 Promise

var promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve();
  console.log(2);
});
promise.then(() => { console.log(3); });
console.log(4);
// 输出:1 2 4 3

构造函数同步执行;resolve() 之后 then 回调进微任务队列。

题目二:未 resolve 的 Promise

var promise = new Promise((resolve, reject) => { console.log(1); });
promise.then(() => { console.log(2); });
console.log(3);
// 输出:1 3

没有 resolve 时 then 一直 pending,不会执行。

题目三:then 直接传入函数

var promise = new Promise((resolve, reject) => { console.log(1); });
promise.then(console.log(2));
console.log(3);
// 输出:1 2 3

console.log(2) 作为参数立即执行,输出 2;返回 undefined 作为 then 回调。

题目四:then 的值穿透

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log);
// 输出:1

then 期望函数,非函数会被忽略,值穿透到下一环。

题目五:resolve + reject

var promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve();
  reject();
});
promise.then(() => { console.log(2); }).catch(() => { console.log(3); });
console.log(4);
// 输出:1 4 2

Promise 状态一旦确定(resolve / reject)就不可再改变。

题目六:return 透传

Promise.resolve(1)
  .then(res => { console.log(res); return 2; })
  .catch(err => { return 3; })
  .then(res => { console.log(res); });
// 输出:1 2

catch 不触发时,then 拿到上一个 then 的 return 值。

5. 同步 + Promise + setTimeout

题目一

setTimeout(() => { console.log(1); });
Promise.resolve().then(() => { console.log(2); });
console.log(3);
// 输出:3 2 1

微任务先于宏任务。

题目二

var promise = new Promise((resolve, reject) => {
  console.log(1);
  setTimeout(() => { console.log(2); resolve(); }, 1000);
});

promise.then(() => { console.log(3); });
promise.then(() => { console.log(4); });
console.log(5);
// 输出:1 5 2 3 4

多个 then 注册多个回调,都会依次执行。

6. 回到开篇题

var date = new Date();

console.log(1, new Date() - date);                       // 同步
setTimeout(() => { console.log(2, new Date() - date); }, 500);  // 宏任务
Promise.resolve().then(console.log(3, new Date() - date));       // 微任务(console.log 作为参数立即执行)
while (new Date() - date < 1000) {}                              // 同步阻塞
console.log(4, new Date() - date);                               // 同步

输出顺序:1 0 → 3 1 → 4 1000 → 2 1000。

7. 总结

  • 同步任务立即执行。
  • 微任务(Promise.then、MutationObserver)在同步任务结束后、当前宏任务结束前清空。
  • 宏任务(setTimeout、I/O)每轮 Event Loop 取一个执行。
  • Promise.then 期望函数,非函数参数会被忽略(值穿透)。
  • 异常穿透:链式调用未指定失败回调时,错误会沿链一路传递到最近的 catch。

来源整理自:我的有道云笔记