好的,我们来系统且深入地讲清楚 JavaScript 的 Promise,从它的“是什么”、“为什么需要它”,到“如何使用”,最后再深入到它的“底层实现原理”。
一、Promise 是什么?为什么需要它?
在理解 Promise 之前,必须先理解 JavaScript 的运行环境:单线程 + 事件循环 (Event Loop)。
单线程意味着代码一次只能做一件事。如果遇到一个耗时操作,比如网络请求(fetch)、文件读写(fs.readFile),整个程序就会被阻塞,用户界面会卡死。这显然是无法接受的。
为了解决这个问题,JavaScript 引入了异步编程模型。最早的方案是回调函数 (Callback)。
JavaScript
// 示例:通过回调函数处理异步操作
function fetchData(url, callback) {
// 模拟网络请求
setTimeout(() => {
const data = { userId: 1, content: '你好' };
callback(data);
}, 1000);
}
fetchData('/api/user', function(data) {
console.log('获取到的数据:', data);
});
这在简单场景下是可行的。但如果多个异步操作之间存在依赖关系,就会产生臭名昭著的回调地狱 (Callback Hell):
JavaScript
// 回调地狱示例
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// ... 无尽的缩进
});
});
});
});
这种代码结构混乱、难以阅读和维护,并且错误处理非常麻烦。
Promise 正是为解决这些问题而诞生的。
Promise 是一个对象,它代表一个尚未完成但最终会完成(或失败)的异步操作的结果。
你可以把它想象成一张“提货单”。你下单后(发起异步操作),立刻拿到一张提货单(Promise 对象)。你不用在原地傻等货物,可以继续做别的事情。当货物到了(操作成功)或者被告知没货了(操作失败),你再凭着这张提货单去处理结果。
这张“提货单”有三个明确的状态:
-
pending(进行中):初始状态,操作既未完成也未失败。 -
fulfilled(已成功):操作成功完成。 -
rejected(已失败):操作失败。
Promise 的一个核心特点是:状态一旦改变,就永远不会再变。从 pending 变为 fulfilled 或 rejected 后,状态就凝固了。这为我们提供了可靠的确定性。
二、如何使用 Promise?
1. 创建 Promise
通过 new Promise 构造函数来创建一个 Promise 实例。构造函数接受一个执行器 (executor) 函数作为参数。
这个执行器函数会立即执行,并接收两个参数:resolve 和 reject。
-
resolve(value): 一个函数,用于将 Promise 的状态从pending变为fulfilled,并传递成功的结果。 -
reject(reason): 一个函数,用于将 Promise 的状态从pending变为rejected,并传递失败的原因。
JavaScript
const myPromise = new Promise((resolve, reject) => {
// 执行一些异步操作
console.log('Promise 开始执行...');
setTimeout(() => {
const success = true; // 模拟成功或失败
if (success) {
resolve('操作成功!这是结果。'); // 状态变为 fulfilled
} else {
reject('操作失败!这是原因。'); // 状态变为 rejected
}
}, 1000);
});
2. 消费 Promise:.then(), .catch(), .finally()
Promise 对象创建后,我们使用它的原型方法来处理最终的结果。
-
.then(onFulfilled, onRejected)-
这是最核心的消费方法。它接收两个可选的函数作为参数。
-
onFulfilled:当 Promise 状态变为fulfilled时被调用,接收成功的值。 -
onRejected:当 Promise 状态变为rejected时被调用,接收失败的原因。
-
-
.catch(onRejected)- 本质上是
.then(null, onRejected)的语法糖,专门用于捕获和处理错误。
- 本质上是
-
.finally(onFinally)- 无论 Promise 最终是
fulfilled还是rejected,onFinally回调都会被执行。它不接收任何参数,通常用于执行清理工作,比如关闭加载动画。
- 无论 Promise 最终是
核心特性:链式调用 (Chaining)
.then(), .catch(), .finally() 都会返回一个新的 Promise。这是实现链式调用的关键,它完美地解决了回调地狱问题。
JavaScript
myPromise
.then(successValue => {
console.log(successValue); // 输出: "操作成功!这是结果。"
// 返回一个新的值,供下一个 .then() 使用
return '处理后的数据';
})
.then(processedValue => {
console.log(processedValue); // 输出: "处理后的数据"
// 也可以返回一个新的 Promise
return new Promise(resolve => setTimeout(() => resolve('第二次异步操作成功'), 500));
})
.then(secondResult => {
console.log(secondResult); // 500ms 后输出: "第二次异步操作成功"
throw new Error('故意抛出一个错误'); // 模拟一个错误
})
.catch(error => {
// 上面链条中任何一个环节的 reject 或 throw 都会被这个 catch 捕获
console.error(error.message); // 输出: "故意抛出一个错误"
})
.finally(() => {
// 无论成功还是失败,这里都会执行
console.log('Promise 流程结束,执行清理工作。');
});
3. 常用静态方法
Promise 对象本身也提供了一些强大的静态方法,用于处理多个 Promise。
-
Promise.all(iterable): 接收一个 Promise 数组。-
成功:当所有 Promise 都变为
fulfilled时,它才会fulfilled,并且结果是一个包含所有 Promise 结果的数组(按传入顺序)。 -
失败:只要有一个 Promise
rejected,它就会立即rejected,并且原因就是那个失败的 Promise 的原因。 -
场景:需要等待多个互不依赖的请求全部完成后再进行下一步操作。
-
-
Promise.race(iterable): 接收一个 Promise 数组。-
它会返回一个“赛跑”的 Promise,这个 Promise 的状态由第一个“冲过终点”(即第一个改变状态)的 Promise 决定。无论是
fulfilled还是rejected。 -
场景:多个数据源,哪个先返回就用哪个;或者设置请求超时。
-
-
Promise.allSettled(iterable): 接收一个 Promise 数组。-
它总是会等待所有的 Promise 都“落定”(settled,即无论是
fulfilled还是rejected)。 -
它的结果是一个对象数组,每个对象描述了对应 Promise 的最终状态(
status)和结果(value)或原因(reason)。 -
场景:当你关心多个异步操作的最终结果,无论它们成功与否时,这个方法非常有用,因为它不会因为一个失败而短路。
-
-
Promise.any(iterable): 接收一个 Promise 数组。-
只要有一个 Promise
fulfilled,它就立即fulfilled,结果是那个成功 Promise 的结果。 -
只有当所有 Promise 都
rejected时,它才会rejected,并且 reason 是一个包含所有失败原因的AggregateError对象。 -
场景:多个数据源,只要有一个成功返回就可以继续。
-
三、深入底层实现原理
要真正理解 Promise,我们可以尝试自己实现一个简化版的 Promise。这能帮助我们看清内部的运作机制。一个符合 Promises/A+ 规范的 Promise 需要以下核心要素:
-
状态 (
state) 和 结果 (value/reason) 的存储。 -
resolve和reject函数来改变状态。 -
.then()方法来注册回调,并支持链式调用。 -
处理异步:当
resolve或reject在异步操作中被调用时,.then()注册的回调需要被正确执行。 -
微任务 (Microtask):Promise 的回调(
.then,.catch,.finally)是异步执行的,它们会被放入微任务队列,而不是宏任务队列。
事件循环与微任务
在深入代码前,必须理解事件循环 (Event Loop)中的微任务 (Microtask) 和 宏任务 (Macrotask)。
-
主线程 (Call Stack):执行同步代码。
-
宏任务队列 (Task Queue):存放
setTimeout,setInterval, I/O 操作等的回调。 -
微任务队列 (Microtask Queue):存放
Promise.then,MutationObserver等的回调。
执行顺序是:
-
执行完当前 Call Stack 中的所有同步代码。
-
检查微任务队列,执行所有微任务。
-
取出一个宏任务来执行。
-
重复步骤 2 和 3。
关键点:微任务总是在当前同步代码执行完毕后、下一个宏任务开始前执行。这就是为什么 Promise.then 的回调会比 setTimeout 的回调先执行。
JavaScript
console.log('start');
setTimeout(() => {
console.log('setTimeout'); // 宏任务
}, 0);
new Promise(resolve => {
console.log('promise executor');
resolve();
}).then(() => {
console.log('promise then'); // 微任务
});
console.log('end');
// 输出顺序:
// start
// promise executor
// end
// promise then
// setTimeout
手写一个 MyPromise
下面是一个简化版的 Promise 实现,它揭示了核心逻辑。
JavaScript
// 定义三个状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
constructor(executor) {
// 初始状态为 pending
this.state = PENDING;
// 存放成功的结果或失败的原因
this.value = undefined;
// 存放 then 方法注册的成功和失败回调
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
// 只有在 pending 状态下才能改变
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
// 依次执行所有注册的成功回调
this.onFulfilledCallbacks.forEach(fn => fn(this.value));
}
};
const reject = (reason) => {
// 只有在 pending 状态下才能改变
if (this.state === PENDING) {
this.state = REJECTED;
this.value = reason;
// 依次执行所有注册的失败回调
this.onRejectedCallbacks.forEach(fn => fn(this.value));
}
};
// executor 函数可能会抛出异常,需要捕获
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
// .then 的核心:返回一个新的 Promise
const promise2 = new MyPromise((resolve, reject) => {
// 封装 onFulfilled 回调,处理其返回值
const fulfilledMicrotask = () => {
// 使用 queueMicrotask 模拟微任务
queueMicrotask(() => {
try {
// 如果 onFulfilled 不是函数,直接将值透传下去
if (typeof onFulfilled !== 'function') {
resolve(this.value);
} else {
const x = onFulfilled(this.value);
// resolvePromise 是一个关键的函数,用来处理 x 的类型
// 可能是普通值,也可能是新的 promise
resolvePromise(promise2, x, resolve, reject);
}
} catch (error) {
reject(error);
}
});
}
const rejectedMicrotask = () => {
queueMicrotask(() => {
try {
if (typeof onRejected !== 'function') {
reject(this.value);
} else {
const x = onRejected(this.value);
resolvePromise(promise2, x, resolve, reject);
}
} catch (error) {
reject(error);
}
});
}
// 根据当前 Promise 的状态决定如何做
if (this.state === FULFILLED) {
fulfilledMicrotask();
} else if (this.state === REJECTED) {
rejectedMicrotask();
} else if (this.state === PENDING) {
// 如果还是 pending,就把回调存起来,等待状态改变后再执行
this.onFulfilledCallbacks.push(fulfilledMicrotask);
this.onRejectedCallbacks.push(rejectedMicrotask);
}
});
return promise2;
}
}
// resolvePromise 负责处理 .then 回调返回值的各种情况
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
if (x instanceof MyPromise) {
// 如果 x 是一个 Promise,则等待它完成
x.then(y => {
resolvePromise(promise2, y, resolve, reject);
}, reject);
} else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// 处理 thenable 对象/函数
let called = false;
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, r => {
if (called) return;
called = true;
reject(r);
});
} else {
if (called) return;
called = true;
resolve(x);
}
} catch(e) {
if (called) return;
called = true;
reject(e);
}
} else {
// 普通值,直接 resolve
resolve(x);
}
}
这份手写代码揭示了几个关键点:
-
状态机:内部通过
state变量管理状态,并保证状态只能从pending单向流转。 -
回调队列:当 Promise 处于
pending状态时,.then注册的回调函数被存入onFulfilledCallbacks和onRejectedCallbacks数组。 -
延迟执行:当
resolve或reject被调用时,才会从回调队列中取出相应的函数来执行。 -
链式调用:
.then方法必须返回一个新的promise2。promise2的状态由onFulfilled或onRejected的执行结果决定。resolvePromise函数就是这个连接新旧 Promise 的桥梁,是实现链式调用的核心所在。 -
微任务:在标准实现中,
.then的回调执行需要被包裹在一个微任务中(这里用queueMicrotask模拟),以确保其异步性,并符合事件循环的规范。
四、现代实践:Async/Await
ES2017 引入的 async/await 是建立在 Promise 之上的语法糖,它让我们能用更像同步代码的风格来编写异步逻辑,极大地提升了可读性。
-
async函数会隐式地返回一个 Promise。 -
await关键字只能在async函数中使用,它会“暂停”函数的执行,等待它后面的 Promise 完成,然后返回 Promise 的结果。如果 Promise 被拒绝,await会抛出错误,可以用try...catch来捕获。
JavaScript
// 使用 Promise.then
function request() {
return new Promise(resolve => setTimeout(() => resolve("done!"), 1000));
}
request()
.then(result => console.log(result))
.catch(err => console.error(err));
// 使用 async/await
async function main() {
try {
console.log("start waiting...");
const result = await request(); // 代码在这里“暂停”,直到 request() 的 promise 完成
console.log(result); // 1秒后输出 "done!"
} catch (err) {
console.error(err);
}
}
main();
async/await 的底层机制完全依赖于 Promise,它并没有引入新的模型,只是让 Promise 的使用变得更加优雅和直观。
总结
-
Promise 的诞生是为了解决回调地狱问题,它提供了一种更优雅、更可控的方式来管理异步操作。
-
核心是状态机 (
pending,fulfilled,rejected) 和不可变性(状态一旦改变,永不更改)。 -
.then的链式调用是其精髓,通过返回新的 Promise,将异步流程线性化,使其易于理解和维护。 -
底层实现依赖于状态管理、回调队列以及与事件循环(特别是微任务队列)的深度整合,保证了执行时机的确定性。
-
async/await是 Promise 的高级封装,是目前编写异步代码的最佳实践,但理解 Promise 依然是掌握现代 JavaScript 异步编程的基石。