Javascript篇 | Promise
前言
Promise 是现代 JavaScript 异步编程的基石,也是面试中的超高频考点。面试官考 Promise,不只是看你会不会用 .then(),而是要考你:
- 对 Promise 状态机制的理解是否透彻
- 能不能手写一个简化版 Promise
- 微任务队列的执行时序能不能分析清楚
- Promise 的各种静态方法(all/race/allSettled/any)有什么区别
很多同学用了几年 async/await,但对 Promise 的底层机制一知半解。面试时一道 Promise 执行顺序题就能暴露问题。今天我们就从状态转换到手写实现,把 Promise 彻底讲透。
诊断自测
在开始之前,试着回答以下问题:
1. 以下代码的输出顺序是什么?
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
2. Promise.all 和 Promise.allSettled 的区别是什么?
3. 以下代码输出什么?
Promise.resolve(1)
.then(val => {
console.log(val);
return val + 1;
})
.then(val => {
console.log(val);
throw new Error('oops');
})
.then(val => {
console.log(val);
})
.catch(err => {
console.log(err.message);
return 'recovered';
})
.then(val => {
console.log(val);
});
点击查看答案
第1题: 输出顺序是 1, 4, 3, 2。同步代码先执行(1, 4),然后微任务(Promise.then → 3),最后宏任务(setTimeout → 2)。
第2题: Promise.all 在任何一个 Promise reject 时就会立即 reject;Promise.allSettled 会等所有 Promise 都完成(无论 resolve 还是 reject),返回每个 Promise 的结果对象。
第3题: 输出 1, 2, 'oops', 'recovered'。第三个 .then 被跳过(因为上一步 throw 了),catch 捕获错误后返回了正常值,后续的 then 正常执行。
如果三道题都答对了,说明你基础不错!继续阅读可以查漏补缺。如果有答错的,别担心,读完本文你就能彻底搞懂。
Promise 的三种状态
Promise 本质上是一个状态机,有且只有三种状态:
- pending(等待中):初始状态,既不是 fulfilled 也不是 rejected
- fulfilled(已完成):操作成功完成
- rejected(已拒绝):操作失败
状态转换规则
这是面试必问的知识点:
- 初始状态是 pending
- pending → fulfilled:调用
resolve(value)时触发 - pending → rejected:调用
reject(reason)时触发 - 状态一旦改变就不可逆:fulfilled 和 rejected 都是终态
const promise = new Promise((resolve, reject) => {
resolve('success');
reject('fail'); // 无效!状态已经是 fulfilled 了
resolve('again'); // 无效!状态已经是 fulfilled 了
});
promise.then(val => console.log(val)); // 'success'
💡 核心:Promise 的状态只能从 pending 变为 fulfilled 或 rejected,一旦变了就”冻结”,后续的 resolve/reject 调用全部无效。
then / catch / finally
then 的两个参数
then 接受两个可选参数:成功回调和失败回调。
promise.then(
value => { /* fulfilled 时执行 */ },
reason => { /* rejected 时执行 */ }
);
但在实际开发中,更推荐用 .catch() 处理 rejected:
promise
.then(value => { /* 处理成功 */ })
.catch(reason => { /* 处理失败 */ });
为什么? 因为 .then(onFulfilled, onRejected) 中的 onRejected 只能捕获上一个 Promise 的错误,而 .catch() 能捕获整条链上的任何错误。
promise
.then(value => {
throw new Error('then 里面抛出的错误');
}, err => {
// ❌ 这里捕获不到上面 then 中的错误
});
promise
.then(value => {
throw new Error('then 里面抛出的错误');
})
.catch(err => {
// ✅ 这里能捕获到
console.log(err.message);
});
链式调用的秘密
.then() 返回的是一个新的 Promise,这就是链式调用的基础:
Promise.resolve(1)
.then(val => val * 2) // 返回新 Promise,resolve(2)
.then(val => val + 3) // 返回新 Promise,resolve(5)
.then(val => {
console.log(val); // 5
});
返回值规则:
- 返回普通值 → 下一个 then 接收这个值
- 返回 Promise → 下一个 then 等待这个 Promise 完成
- 抛出错误 → 下一个 catch 接收错误
- 没有返回值 → 下一个 then 接收 undefined
Promise.resolve('start')
.then(val => {
console.log(val); // 'start'
return new Promise(resolve => {
setTimeout(() => resolve('async result'), 1000);
});
})
.then(val => {
console.log(val); // 'async result'(等待 1 秒后)
// 没有 return
})
.then(val => {
console.log(val); // undefined
});
finally
finally 不管 Promise 是 fulfilled 还是 rejected 都会执行,常用于清理操作:
fetchData()
.then(data => processData(data))
.catch(err => handleError(err))
.finally(() => {
hideLoading(); // 无论成功失败都要隐藏 loading
});
finally 的回调不接受参数,它的返回值也不影响链的传递(除非抛出错误或返回 rejected Promise):
Promise.resolve('hello')
.finally(() => {
return 'world'; // 这个返回值会被忽略
})
.then(val => {
console.log(val); // 'hello',不是 'world'
});
Promise 的静态方法对比
这是面试的重点,必须烂熟于心。
Promise.all
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(values => {
console.log(values); // [1, 2, 3]
});
规则:
- 所有 Promise 都 fulfilled → 返回所有结果的数组
- 任何一个 reject → 立即 reject,返回第一个 reject 的原因
const p1 = Promise.resolve(1);
const p2 = Promise.reject('fail');
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).catch(err => {
console.log(err); // 'fail'
});
Promise.race
const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 2000));
const fast = new Promise(resolve => setTimeout(() => resolve('fast'), 1000));
Promise.race([slow, fast]).then(val => {
console.log(val); // 'fast'
});
规则: 返回第一个完成(fulfilled 或 rejected)的 Promise 的结果。
Promise.allSettled
const p1 = Promise.resolve(1);
const p2 = Promise.reject('fail');
const p3 = Promise.resolve(3);
Promise.allSettled([p1, p2, p3]).then(results => {
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: 'fail' },
// { status: 'fulfilled', value: 3 }
// ]
});
规则: 等所有 Promise 都完成,无论成功失败。返回结果对象数组,每个对象有 status 字段。
Promise.any
const p1 = Promise.reject('err1');
const p2 = Promise.resolve('success');
const p3 = Promise.reject('err3');
Promise.any([p1, p2, p3]).then(val => {
console.log(val); // 'success'
});
规则:
- 返回第一个 fulfilled 的 Promise 的值
- 如果所有都 reject,返回
AggregateError
四者对比表
| 方法 | 成功条件 | 失败条件 | 返回值 |
|---|---|---|---|
all | 全部 fulfilled | 任一 rejected | 结果数组 / 第一个错误 |
race | 第一个完成 | 第一个完成(如果是 reject) | 第一个结果 |
allSettled | 永远 fulfilled | 不会 reject | 结果对象数组 |
any | 任一 fulfilled | 全部 rejected | 第一个成功值 / AggregateError |
💡 记忆口诀:all 要全过,race 看谁快,allSettled 都要等,any 有一个就行。
微任务队列与 Promise 的执行时序
这是面试中最容易出错的地方。
宏任务 vs 微任务
JavaScript 有两种异步任务队列:
- 宏任务(macrotask):setTimeout、setInterval、I/O、UI 渲染
- 微任务(microtask):Promise.then/catch/finally、MutationObserver、queueMicrotask
执行顺序: 每个宏任务执行完后,会清空所有微任务,然后再执行下一个宏任务。
console.log('script start'); // 1. 同步
setTimeout(() => {
console.log('setTimeout'); // 5. 宏任务
}, 0);
Promise.resolve()
.then(() => {
console.log('promise1'); // 3. 微任务
})
.then(() => {
console.log('promise2'); // 4. 微任务
});
console.log('script end'); // 2. 同步
// 输出顺序:script start → script end → promise1 → promise2 → setTimeout
经典面试题:复杂的执行顺序
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
new Promise((resolve) => {
console.log('4'); // Promise 构造函数中的代码是同步执行的!
resolve();
}).then(() => {
console.log('5');
});
setTimeout(() => {
console.log('6');
}, 0);
console.log('7');
点击查看执行分析
第一轮(同步代码 / 主脚本):
- 输出
1 - 注册 setTimeout 回调(宏任务队列)
- 执行 Promise 构造函数,输出
4,resolve() .then回调放入微任务队列- 注册另一个 setTimeout 回调(宏任务队列)
- 输出
7
清空微任务队列:
7. 输出 5
第二轮(第一个 setTimeout):
8. 输出 2
9. .then 回调放入微任务队列
清空微任务队列:
10. 输出 3
第三轮(第二个 setTimeout):
11. 输出 6
最终输出: 1, 4, 7, 5, 2, 3, 6
手写简化版 Promise
面试中经常要求手写 Promise,我们来一步步实现核心功能。
第一步:基础结构
class MyPromise {
constructor(executor) {
this.status = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
}
第二步:实现 then
then 是 Promise 的核心方法,需要处理三种情况:已完成、已拒绝、待定。
class MyPromise {
// ... constructor 同上
then(onFulfilled, onRejected) {
// 参数穿透:如果不是函数,就用默认函数
onFulfilled = typeof onFulfilled === 'function'
? onFulfilled
: value => value;
onRejected = typeof onRejected === 'function'
? onRejected
: reason => { throw reason; };
// then 必须返回新的 Promise
const promise2 = new MyPromise((resolve, reject) => {
const handleFulfilled = () => {
// 用微任务模拟异步
queueMicrotask(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
});
};
const handleRejected = () => {
queueMicrotask(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
});
};
if (this.status === 'fulfilled') {
handleFulfilled();
} else if (this.status === 'rejected') {
handleRejected();
} else {
// pending 状态,先存起来
this.onFulfilledCallbacks.push(handleFulfilled);
this.onRejectedCallbacks.push(handleRejected);
}
});
return promise2;
}
}
第三步:处理返回值
resolvePromise 处理 then 回调的返回值,是符合 Promise/A+ 规范的关键:
function resolvePromise(promise2, x, resolve, reject) {
// 防止循环引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected'));
}
if (x instanceof MyPromise) {
// 如果返回的是 Promise,等待其完成
x.then(resolve, reject);
} else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// thenable 对象
try {
const then = x.then;
if (typeof then === 'function') {
let called = false;
then.call(
x,
y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
r => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(x);
}
} catch (err) {
reject(err);
}
} else {
// 普通值,直接 resolve
resolve(x);
}
}
第四步:添加静态方法
class MyPromise {
// ... 以上代码
catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
if (value instanceof MyPromise) return value;
return new MyPromise(resolve => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let count = 0;
const len = promises.length;
if (len === 0) return resolve([]);
promises.forEach((p, i) => {
MyPromise.resolve(p).then(
value => {
results[i] = value;
if (++count === len) resolve(results);
},
reject
);
});
});
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(p => {
MyPromise.resolve(p).then(resolve, reject);
});
});
}
}
面试时不需要写得这么完整,重点是展示你理解 Promise 的核心机制:状态机 + 回调队列 + 链式调用 + 微任务。
async/await 与 Promise 的关系
async/await 是什么?
async/await 是 Promise 的语法糖,让异步代码看起来像同步代码:
// Promise 写法
function fetchUser() {
return fetch('/api/user')
.then(res => res.json())
.then(data => {
console.log(data);
return data;
})
.catch(err => console.error(err));
}
// async/await 写法
async function fetchUser() {
try {
const res = await fetch('/api/user');
const data = await res.json();
console.log(data);
return data;
} catch (err) {
console.error(err);
}
}
本质关系
async函数始终返回一个 Promiseawait会暂停 async 函数的执行,等待 Promise 完成await后面的代码相当于放在.then()中
async function foo() {
console.log('1');
const result = await Promise.resolve('2');
console.log(result);
console.log('3');
}
// 等价于:
function foo() {
console.log('1');
return Promise.resolve('2').then(result => {
console.log(result);
console.log('3');
});
}
经典面试题:async/await 的执行顺序
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
async1();
console.log('script end');
点击查看分析
script start
async1 start
async2
script end
async1 end
- 输出
script start - 调用
async1(),输出async1 start - 执行
await async2(),调用async2(),输出async2 await暂停async1,async1 end被放入微任务队列- 回到主脚本,输出
script end - 清空微任务队列,输出
async1 end
错误处理
async/await 的错误处理用 try/catch,比 Promise 链的 .catch() 更直观:
async function riskyOperation() {
try {
const data = await fetchSomething();
const result = await processSomething(data);
return result;
} catch (err) {
// 可以捕获上面任何一步的错误
console.error('操作失败:', err);
throw err; // 如果需要向上传播
}
}
常见误区
误区1:Promise 构造函数中的代码是异步的
很多人以为 new Promise(fn) 中的 fn 是异步执行的。实际上它是同步的!
console.log('before');
new Promise((resolve) => {
console.log('inside Promise'); // 同步执行!
resolve();
});
console.log('after');
// 输出:before → inside Promise → after
只有 .then() / .catch() / .finally() 的回调才是微任务(异步)。
误区2:catch 会终止 Promise 链
catch 不会终止链,它会返回一个新的 fulfilled Promise(除非 catch 中又抛出错误):
Promise.reject('error')
.catch(err => {
console.log('caught:', err);
return 'recovered'; // 返回正常值
})
.then(val => {
console.log('then:', val); // 'then: recovered' ✅ 正常执行
});
误区3:await 可以用在任何函数中
await 只能在 async 函数内部使用(或 ES Module 的顶层):
// ❌ 错误
function foo() {
const result = await fetchData(); // SyntaxError
}
// ✅ 正确
async function foo() {
const result = await fetchData();
}
// ✅ 顶层 await(ES Module)
const data = await fetch('/api/data');
误区4:Promise.all 中一个失败全部结果都丢失
有些同学认为 Promise.all 中一个 reject 了,其他 Promise 的结果就完全拿不到了。其实其他 Promise 仍然会执行完成,只是 Promise.all 返回的结果只包含第一个 reject 的原因。
如果需要所有结果(包括失败的),用 Promise.allSettled:
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
]);
// 可以分别处理成功和失败的结果
const successes = results.filter(r => r.status === 'fulfilled');
const failures = results.filter(r => r.status === 'rejected');
小结
Promise 是 JavaScript 异步编程的核心,掌握了它就掌握了异步的半壁江山。
核心要点
- 三种状态:pending → fulfilled / rejected,不可逆
- then 返回新 Promise:这是链式调用的基础
- 微任务:Promise 回调是微任务,优先于宏任务执行
- 静态方法:all(全过)、race(谁快)、allSettled(都等)、any(有一个)
- async/await:Promise 的语法糖,await 后面的代码等价于 then 回调
- 手写核心:状态机 + 回调队列 + resolvePromise 处理返回值
本章思维导图
- 三种状态
- pending(初始)
- fulfilled(成功)
- rejected(失败)
- 状态不可逆
- 实例方法
- then(链式调用核心)
- catch(错误捕获)
- finally(清理操作)
- 静态方法
- Promise.all(全部成功)
- Promise.race(谁先完成)
- Promise.allSettled(全部完成)
- Promise.any(任一成功)
- Promise.resolve
- Promise.reject
- 执行时序
- 构造函数同步执行
- then/catch/finally 是微任务
- 微任务优先于宏任务
- 每轮宏任务后清空微任务
- 手写实现
- 状态机(status/value/reason)
- 回调队列(pending 时存储)
- then 返回新 Promise
- resolvePromise 处理返回值
- async/await
- async 返回 Promise
- await 暂停执行
- 等价于 then 回调
- try/catch 错误处理
练习挑战
挑战一:基础(⭐)
以下代码的输出是什么?
const p = new Promise((resolve, reject) => {
resolve('first');
resolve('second');
reject('error');
});
p.then(val => console.log(val));
p.catch(err => console.log(err));
答案与解析
输出:first
Promise 状态只能改变一次。第一次调用 resolve('first') 后状态变为 fulfilled,后续的 resolve 和 reject 都无效。.catch 不会触发,因为 Promise 是 fulfilled 状态。
挑战二:进阶(⭐⭐)
写出以下代码的输出顺序:
Promise.resolve()
.then(() => {
console.log('A');
return Promise.resolve('B');
})
.then(val => {
console.log(val);
});
Promise.resolve()
.then(() => {
console.log('C');
})
.then(() => {
console.log('D');
})
.then(() => {
console.log('E');
});
答案与解析
输出顺序:A, C, D, B, E
关键点:当 .then 回调返回一个 Promise 时,需要额外的微任务来处理(V8 中通常需要两个额外的微任务 tick)。所以 B 的输出会被推迟。
详细分析:
- 两个 Promise 链的第一个 then 都入微任务队列
- 执行第一个:输出
A,返回Promise.resolve('B')(需要额外 microtick) - 执行第二个:输出
C,D入队 - 处理返回的 Promise(额外 microtick 1)
- 执行
D,E入队 - 处理返回的 Promise(额外 microtick 2),
B入队 - 输出
E…不,这里需要更精确的分析——实际上是B先于E
注意:这道题的精确输出可能因 JavaScript 引擎版本不同略有差异。V8 最新版本的结果是 A, C, D, B, E。
挑战三:综合(⭐⭐⭐)
实现一个 promiseTimeout 函数,如果传入的 Promise 在指定时间内没有 resolve,就自动 reject:
function promiseTimeout(promise, ms) {
// 你的实现
}
// 使用示例
const slow = new Promise(resolve => setTimeout(() => resolve('done'), 5000));
promiseTimeout(slow, 2000)
.then(val => console.log(val))
.catch(err => console.log(err)); // 'Timeout after 2000ms'
答案与解析
function promiseTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Timeout after ${ms}ms`));
}, ms);
});
return Promise.race([promise, timeout]);
}
核心思路:利用 Promise.race,把原始 Promise 和一个定时 reject 的 Promise 放在一起竞争。谁先完成就用谁的结果。如果原始 Promise 在超时前完成,就返回其结果;否则就被 timeout Promise reject。
进阶版——支持取消超时定时器(避免内存泄漏):
function promiseTimeout(promise, ms) {
let timeoutId;
const timeout = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error(`Timeout after ${ms}ms`));
}, ms);
});
return Promise.race([promise, timeout]).finally(() => {
clearTimeout(timeoutId);
});
}
自我检测
读完本章后,确认你能回答以下问题:
- 能说出 Promise 三种状态及转换规则(单向不可逆)
- 能解释 then 返回新 Promise 是链式调用的基础
- 能区分 Promise.all / race / allSettled / any 的行为差异
- 能解释为什么 Promise 构造函数中的代码是同步执行的
- 能分析复杂的 Promise + setTimeout 混合执行顺序
- 能手写简化版 Promise(至少包含 constructor + then + resolve + reject)
- 能解释 async/await 与 Promise 的等价关系
- 理解 catch 不会终止 Promise 链,它返回的是 fulfilled Promise
- 能解释微任务和宏任务的执行顺序
- 能在实际场景中选择合适的 Promise 静态方法
购买课程解锁全部内容
大厂前端面试通关:71 篇构建完整知识体系
¥89.90