前端 Promise 全解:从原理到面试
重点总结定义:Promise 是异步编程的一种解决方案,解决了回调地狱问题,提供了统一的 API。状态:它有 Pending、Fulfilled、Rejected 三种状态,且状态不可逆。使用:构造函数是同步执行的。.then 返回新 Promise,支持链式调用。.catch 处理错误(冒泡机制)。常用 API:all(并发,全对才对)。race(赛跑,谁快用谁)。allSettled(不管死活,都要结果)。any(只要有一个活的就行)。运行机制:Promise 的回调是微任务,在同步代码执行完后、宏任务执行前执行,这涉及到了 Event Loop。最佳实践:现在项目中主要配合 async/await 使用,使异步代码看起来像同步代码,可读性更强。
1. 什么是 Promise?(核心概念)
一句话定义: Promise 是异步编程的一种解决方案,它是一个对象,代表了一个异步操作的最终完成(或失败)及其结果值。
1.1 为什么要用 Promise?
在 Promise 出现之前,我们处理异步主要靠回调函数(Callback)。当异步操作依赖另一个异步操作时,就会出现“回调地狱”(Callback Hell):
// 噩梦般的回调地狱 doSomething(function(result) { doSomethingElse(result, function(newResult) { doThirdThing(newResult, function(finalResult) { console.log('Got the final result: ' + finalResult); }, failureCallback); }, failureCallback); }, failureCallback);Promise 的作用: 将嵌套的回调改为链式调用,解决了回调地狱问题,使代码更具可读性和逻辑性,同时提供了更好的错误处理机制。
1.2 Promise 的三种状态
Promise 是一个状态机,它拥有三种状态:
- Pending(进行中):初始状态,既没有被兑现,也没有被拒绝。
- Fulfilled(已成功/Resolved):意味着操作成功完成。
- Rejected(已失败):意味着操作失败。
重要特性:状态不可逆
一旦状态从 Pending 变为 Fulfilled 或 Rejected,状态就固定了,不会再发生改变。这被称为 "Settled"(已决议)。
2. Promise 的基本使用与实例方法
2.1 构造函数 (Executor)
const p = new Promise((resolve, reject) => { // Executor 执行器函数,是【同步执行】的! console.log('1. Executor runs immediately'); // 模拟异步 setTimeout(() => { const success = true; if (success) { resolve('Success Data'); // 状态变成 Fulfilled } else { reject('Error Reason'); // 状态变成 Rejected } }, 1000); });面试考点: new Promise(fn) 中的 fn 是立即同步执行的。
2.2 .then(onFulfilled, onRejected)
- 作用:接收 Promise 成功或失败的结果。
- 返回值:返回一个新的 Promise(这是链式调用的关键)。
- 特性:
- 如果回调函数返回一个普通值,新 Promise 状态为 Fulfilled,值为该返回值。
- 如果回调函数抛出错误,新 Promise 状态为 Rejected。
- 如果回调函数返回一个 Promise,新 Promise 将等待这个 Promise 的状态。
- 值穿透:如果 .then() 没传回调函数,值会原样向后传递。
2.3 .catch(onRejected)
- 作用:捕获 Promise 链中的错误。
- 本质:它只是 .then(null, onRejected) 的语法糖。
- 冒泡性:错误会沿着链一直向下传递,直到被最近的 catch 捕获。
2.4 .finally(onFinally)
- 作用:无论 Promise 是成功还是失败,都会执行。
- 场景:关闭 Loading 动画、关闭数据库连接等清理工作。
- 注意:finally 不接收任何参数,它也不知道前面的状态是啥。它返回的 Promise 通常会延续上一个 Promise 的结果(除非 finally 里面抛错)。
3. Promise 静态方法 (API 详解)
这是面试中最常考察应用场景的部分。
3.1 Promise.resolve(value)
- 作用:将现有对象转为 Promise 对象(状态为 Fulfilled)。
- 场景:当你需要一个确定的成功结果进行后续链式调用时。
3.2 Promise.reject(reason)
- 作用:返回一个状态为 Rejected 的 Promise 实例。
3.3 Promise.all([p1, p2, p3])
- 逻辑:"并发执行,全对才对,一错全错"。
- 输入:一个 Promise 数组(Iterator)。
- 输出:
- 成功:当所有 Promise 都成功时,返回一个数组,包含所有结果(顺序与输入一致)。
- 失败:只要有一个失败,立即返回那个失败的原因(短路效应)。
- 场景:页面加载时,同时请求用户信息、菜单列表、系统配置,三个都拿到才能渲染页面。
3.4 Promise.race([p1, p2, p3])
- 逻辑:"赛跑机制,谁快听谁的"。
- 输出:返回第一个改变状态(无论是成功还是失败)的 Promise 的结果。
- 场景:
- 请求超时控制:比如网络请求和 setTimeout 赛跑,如果 5秒没回来,setTimeout 赢了,抛出超时错误。
3.5 Promise.allSettled([p1, p2, p3]) (ES2020)
- 逻辑:"我全都要,不管对错"。
- 输出:等待所有 Promise 都结束(Settled)。返回一个数组,每个对象包含 status ('fulfilled'/'rejected') 和对应的 value 或 reason。
- 场景:你需要知道所有请求的结果,不关心其中某个是否失败。例如批量上传图片,失败几张没关系,需要知道哪几张成功哪几张失败。
3.6 Promise.any([p1, p2, p3]) (ES2021)
- 逻辑:"只要有一个成就算成功"。
- 输出:
- 只要有一个 Promise 成功,就返回那个成功的。
- 只有当所有 Promise 都失败时,才返回失败(AggregateError)。
- 场景:从多个 CDN 节点加载同一张图片,只要有一个加载出来就行。
4. 进阶:Promise 与事件循环 (Event Loop)
这是区分初级和中高级开发者的分水岭。
4.1 微任务 (Microtask)
Promise 的 .then、.catch、.finally 中的回调函数属于 微任务。
setTimeout、setInterval 属于 宏任务 (Macrotask)。
4.2 执行顺序
- 执行同步代码(包括 new Promise 构造函数内部的代码)。
- 同步代码执行完,检查并执行所有微任务队列(Promise 回调)。
- 渲染 DOM(如果有必要)。
- 执行一个宏任务。
- 回到步骤 2 循环。
经典面试题:
console.log('1'); setTimeout(() => { console.log('2'); }, 0); Promise.resolve().then(() => { console.log('3'); }); new Promise((resolve) => { console.log('4'); resolve(); }).then(() => { console.log('5'); }); console.log('6');答案与解析:
输出顺序:1 -> 4 -> 6 -> 3 -> 5 -> 2
- 1: 同步。
- setTimeout: 放入宏任务队列。
- 3: 放入微任务队列。
- 4: new Promise 是同步执行的。
- 5: 放入微任务队列。
- 6: 同步。
- 同步结束,清空微任务:输出 3, 5。
- 微任务清空,执行宏任务:输出 2。
5. 面试常见“手写”题思路
面试官可能会让你手写 Promise.all 或简易版 Promise。
5.1 手写 Promise.all
核心逻辑:
- 返回一个新的 Promise。
- 遍历输入数组。
- 用 Promise.resolve 包装每一项(防止数组里有非 Promise 值)。
- 维护一个计数器 count 和结果数组 results。
- 每成功一个,results[i] = value,count++。
- 如果 count === length,调用 resolve(results)。
- 只要有一个失败,直接调用 reject(reason)。
Promise.myAll = function(promises) { return new Promise((resolve, reject) => { let results = []; let count = 0; if(promises.length === 0) resolve([]); promises.forEach((p, index) => { Promise.resolve(p).then(res => { results[index] = res; // 保证结果顺序和输入顺序一致 count++; if (count === promises.length) { resolve(results); } }, err => { reject(err); }) }) }) }6. Promise 的终极形态:Async/Await
面试必问:Async/Await 和 Promise 的关系?
- async/await 是 Generator + Promise 的语法糖。
- async 函数返回的一定是一个 Promise。
- await 后面通常接 Promise,它会暂停 async 函数的执行,等待 Promise 解决,并把结果作为 await 表达式的值。
- 优势:用同步的代码逻辑写异步代码,彻底解决了 .then 链过长的问题,try...catch 可以同时捕获同步和异步错误。