Promise 对象 是现代 JavaScript 中异步编程的基础,是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象。
Promise 对象
在基于 Promise 的 API 中,异步函数会启动操作并返回 Promise 对象。
在 Promise 对象返回给调用者的时候,操作往往还没有完成,但 Promise 对象可以指示当前操作所处的状态,并且让我们在操作完成时对其进行处理(无论成功还是失败)。
例如 fetch 函数会返回一个 Promise 对象:
const fetchPromise = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
我们也可以创建自己的 Promise 对象。
Promise 对象的状态
Promise 有三种状态:
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。这是调用 fetch() 返回 Promise 时的状态,此时请求还在进行中。
- 已兑现(fulfilled):意味着操作成功完成。当 Promise 完成时,它的 then() 处理函数被调用。
- 已拒绝(rejected):意味着操作失败。当一个 Promise 失败时,它的 catch() 处理函数被调用。
有时我们用 已敲定(settled)这个词来同时表示已兑现(fulfilled)和已拒绝(rejected)两种情况。当其中任意一种情况发生时,Promise 的 处理函数 将被调用。如果绑定相应处理函数时 Promise 已经兑现或拒绝,这处理函数将被立即调用,因此在异步操作完成和绑定处理函数之间不存在竞态条件。
已敲定的 Promise 状态无法更改。
如果一个 Promise 已敲定,或者如果它被 锁定(lock-in)以跟随另一个 Promise 的状态,那么它就是 已解决(resolved)的。在实践中,“解决”过程通常是在幕后完成的,不可观察,只有其兑现或拒绝是可观察的。
为 Promise 对象附加处理函数
可以将 处理函数 附加到 Promise 对象上,当操作完成时(成功或失败),这些处理函数将被执行。
then() 方法
调用 Promise 的 then() 方法,并传入处理函数,可以为 Promise 对象附加处理函数。
then() 方法接受最多两个参数 onFulfilled, onRejected:第一个参数是 Promise 兑现时的回调函数,第二个参数是 Promise 拒绝时的回调函数。当 Promise 兑现或拒绝时,Promise 将调用我们的处理函数,传入兑现值或者拒绝的原因。
-
如果
onFulfilled不是一个函数,则内部会被替换为一个恒等函数((x) => x),它只是简单地将兑现值向前传递。 -
如果
onRejected不是一个函数,则内部会被替换为一个抛出器函数((x) => { throw x; }),它会抛出它收到的拒绝原因。
例如,如果 fetch 获取数据成功,fetchPromise 将兑现,附加的处理函数将被调用,并且传入兑现值:
fetchPromise.then((response) => {
console.log(`已收到响应:${response.status}`);
});
catch() 方法
catch() 其实就是一个没有为 Promise 兑现时的回调函数留出空位的 .then()。
catch(onRejected) 方法是 then(undefined, onRejected) 的一种简写形式。
finally() 方法
finally() 方法类似于调用 then(onFinally, onFinally)。然而,有几个不同之处:
- 创建内联函数时,你可以只将其传入一次,而不是强制声明两次或为其创建变量。
onFinally回调函数不接收任何参数。这种情况恰好适用于你不关心拒绝原因或兑现值的情况,因此无需提供它。finally()调用通常是透明的,不会更改原始 Promise 的状态。例如:- 与
Promise.resolve(2).then(() => 77, () => {})不同,它返回一个最终会兑现为值 77 的 promise,而Promise.resolve(2).finally(() => 77)返回一个最终兑现为值 2 的 promise。 - 类似地,与
Promise.reject(3).then(() => {}, () => 88)不同,它返回一个最终兑现为值 88 的 promise,而Promise.reject(3).finally(() => 88)返回一个最终以原因 3 拒绝的 promise。
- 与
处理函数不会收到 Thenable 对象
沿着 then() 链产生的 Thenable 对象总是会被解决——onFulfilled 处理函数永远不会收到 Thenable 对象,并且任何由处理函数返回的 Thenable 对象在传递给下一个处理函数之前总是被解决。这是因为在构造新的 Promise 对象时,由 executor 传递的 resolve 和 reject 函数被保存下来,在当前 Promise 对象敲定时,相应的函数将被调用,并传递兑现值或拒绝原因。解决逻辑来自 Promise() 构造函数传递的解决函数。
为 Promise 指定多个处理函数
then() 方法返回一个新的 Promise 对象。如果在同一 Promise 对象上两次调用 then() 方法(而不是链式调用),则该 Promise 对象将具有两对处理方法。附加到同一 Promise 对象的所有处理方法总是按照它们添加的顺序调用。此外,每次调用 then() 方法返回的两个 Promise 对象开始了独立的链,不会等待彼此的敲定。
一个 promise 可能会参与不止一次的嵌套。对于下面的代码,promiseA 向“已敲定”状态的过渡会导致两个实例的 .then() 都被调用。
const promiseA = new Promise(myExecutorFunc);
const promiseB = promiseA.then(handleFulfilled1, handleRejected1);
const promiseC = promiseA.then(handleFulfilled2, handleRejected2);
为已敲定的 Promise 指定处理函数
一个已经处于“已敲定”状态的 promise 也可以接收操作。在那种情况下,(如果没有问题的话)这个操作会被作为第一个异步操作被执行。注意,所有的 promise 都一定是异步的。因此,一个已经处于“已敲定”状态的 promise 中的操作只有 promise 链式调用的栈被清空且一个时间片段过去之后才会被执行。这种效果跟 setTimeout(action, 10) 特别相似。
const promiseA = new Promise((resolve, reject) => {
resolve(777);
});
// 此时,“promiseA”已经敲定了
promiseA.then((val) => console.log("异步日志记录有值:", val));
console.log("立即记录");
// 按以下顺序产生输出:
// 立即记录
// 异步日志记录有值:777
链式 Promise
当前 Promise 对象的 then() 方法会立即返回一个新生成的 Promise 对象,这个对象可被用于链式调用。
新生成的 Promise 对象为待定状态,无论当前 Promise 对象的状态如何。
const p = fetchPromise.then(onFulfilled, onRejected);
如果当前 Promise 对象敲定,处理函数 (onFulfilled, onRejected) 之一将会执行。返回的 Promise 对象(称之为 p)的行为取决于处理函数的执行结果,遵循一组特定的规则。如果处理函数:
- 返回一个值:
p以该返回值作为其兑现值。 - 没有返回任何值:
p以undefined作为其兑现值。 - 抛出一个错误:
p抛出的错误作为其拒绝值。 - 返回一个已兑现的 Promise 对象:
p以该 Promise 的值作为其兑现值。 - 返回一个已拒绝的 Promise 对象:
p以该 Promise 的值作为其拒绝值。 - 返回另一个待定的 Promise 对象:
p保持待定状态,并在该 Promise 对象被兑现/拒绝后立即以该 Promise 的值作为其兑现/拒绝值。
例如,下面的代码创建了三个新的待定 Promise 对象,当 fetchPromise 敲定后,会执行处理函数,然后根据处理函数的执行结果决定下一个 Promise 的状态:
fetchPromise
.then((response) => {
return response.json();
})
.then((json) => {
console.log(json[0].name);
})
.catch((error) => {
console.error(`无法获取产品列表:${error}`);
});
如果将 catch() 添加到 Promise 链的末尾,它就可以在任何异步函数失败时被调用。于是,我们就可以将一个操作实现为几个连续的异步函数调用,并在一个地方处理所有错误。
Promise 并发
Promise 类提供了四个静态方法来促进异步任务的并发:
Promise.all()
由 Promise.all() 返回的 Promise:
- 当且仅当数组中所有的 Promise 都被兑现时,才会通知
then()处理函数并提供一个包含所有响应的数组,数组中响应的顺序与被传入all()的 Promise 的顺序相同。 - 会被拒绝——如果数组中有任何一个 Promise 被拒绝。此时,
catch()处理函数被调用,并提供被拒绝的 Promise 所抛出的错误。
Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
.then((responses) => {
for (const response of responses) {
console.log(`${response.url}:${response.status}`);
}
})
.catch((error) => {
console.error(`获取失败:${error}`);
});
Promise.any()
在 Promise 数组中的任何一个被兑现时它就会被兑现,如果所有的 Promise 都被拒绝,它也会被拒绝。
Promise.race()
Promise.race() 静态方法接受一个 Promise 可迭代对象作为输入,并返回一个 Promise。这个返回的 Promise 会随着第一个 Promise 的敲定而敲定。
Promise.race([promise1, promise2]).then((value) => {
console.log(value);
});
Promise.allSettled()
在所有的 Promise 都被敲定时兑现。
创建自定义 Promise
有以下方法可以创建自定义 Promise,并且手动决定 Promise 的兑现或者拒绝。
使用 Promise 构造函数
通过构造函数 new Promise() 来创建一个自定义 Promise。
构造函数接受一个参数 executor 执行函数。
执行函数接收两个函数作为参数:resolveFunc 和 rejectFunc。在执行函数中调用这两个函数来控制 Promise 的状态。
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('foo');
}, 300);
});
Promise() 只能通过 new 运算符来构造。如果尝试在没有使用 new 的情况下调用它,会抛出 TypeError 异常。
执行函数 executor
executor 是将回调函数的结果与 Promise 关联在一起的自定义代码。它的函数签名应该是:
function executor(resolveFunc, rejectFunc) {
// 通常,`executor` 函数用于封装某些接受回调函数作为参数的异步操作,比如上面的 `readFile` 函数
}
resolveFunc 和 rejectFunc 也是函数,你可以给它们任何实际的名称。它们的函数签名很简单:它们接受一个任意类型的参数。
resolveFunc(value); // 解决时调用
rejectFunc(reason); // 拒绝时调用
传入 resolveFunc 的 value 参数可以是另一个 Promise 对象,在这种情况下,新构造的 Promise 对象的状态将“锁定”到传入的 Promise 对象(作为 resolution Promise 的一部分)。
rejectFunc 的语义类似于 throw 语句,因此 reason 通常是一个 Error 实例。
如果省略 value 或 reason,Promise 将会被兑现(fulfilled)或拒绝(rejected)为 undefined。
executor 函数的返回值会被忽略。executor 函数中的 return 语句仅影响控制流程,调整函数某个部分是否执行,但不会影响 Promise 的兑现值。如果 executor 函数退出,且未来不可能调用 resolveFunc 或 rejectFunc(例如,没有安排异步任务),那么 Promise 将永远保持待定状态。
备注:待定的 Promise 的存在并不会阻止程序退出。如果事件循环为空,则程序会退出,尽管存在待定的 Promise(因为它们必然永远处于待定状态)。
如果在 executor 函数中抛出错误,则 Promise 将被拒绝,除非 resolveFunc 或 rejectFunc 已经被调用。
在构造函数生成新的 Promise 对象时,它还会生成一对相应的 resolveFunc 和 rejectFunc 函数;它们与 Promise 对象“绑定”在一起。
executor 是同步调用的(在构造 Promise 时立即调用),并将 resolveFunc 和 rejectFunc 函数作为传入参数。
更改 Promise 状态
异步任务的最终完成通过 resolveFunc 或 rejectFunc 引起的副作用与 Promise 实例进行通信。这个副作用让 Promise 对象变为“已解决”状态。
如果先调用 resolveFunc,则传入的值将解决。Promise 可能会
- 保持待定状态(如果传入了另一个 thenable 对象),
- 变为已兑现状态(在传入非 thenable 值的大多数情况下),
- 变为已拒绝状态(在解析值无效的情况下)。
如果先调用 rejectFunc,则 Promise 立即变为已拒绝状态。
一旦 resolveFunc 或 rejectFunc 中的一个被调用,Promise 将保持解决状态。只有第一次调用 resolveFunc 或 rejectFunc 会影响 Promise 的最终状态,随后对任一函数的调用都不能更改兑现值或拒绝原因,也不能将其最终状态从“已兑现”转换为“已拒绝”或相反。
如果 executor 抛出错误,则 Promise 被拒绝。但是,如果 resolveFunc 或 rejectFunc 中的一个已经被调用(因此 Promise 已经被解决),则忽略该错误。
解决 Promise 不一定会导致 Promise 变为已兑现或已拒绝(即已敲定)。Promise 可能仍处于待定状态,因为它可能是用另一个 thenable 对象解决的,但它的最终状态将与已解决的 thenable 对象一致。
一旦 Promise 敲定,它会(异步地)调用任何通过 then()、catch() 或 finally() 关联的进一步处理程序。最终的兑现值或拒绝原因在调用时作为输入参数传给兑现和拒绝处理程序。
resolveFunc 的行为
如果它被调用时传入了新建的 Promise 对象本身(即它所“绑定”的 Promise 对象),则 Promise 对象会被拒绝并抛出一个 TypeError 错误。
const rejectedResolved1 = new Promise((resolve) => {
// 注意:resolve 必须异步调用,以便初始化 rejectedResolved1 变量
setTimeout(() => resolve(rejectedResolved1)); // TypeError: Chaining cycle detected for promise #<Promise>
});
如果它使用一个非 thenable 的值(基本类型,或一个没有 then 属性或 then 属性不可调用的对象),则该 Promise 对象会被立即以该值兑现。
new Promise((resolve, reject) => {
resolve(1);
});
// Promise {<fulfilled>: 1}
如果它被调用时传入了一个 thenable 对象(包括另一个 Promise 实例),则该 thenable 对象的 then 方法将被保存并在未来被调用(它总是异步调用)。then 方法将被调用并传入两个回调函数,这两个函数的行为与传递给 executor 函数的 resolveFunc 和 rejectFunc 函数完全相同。如果调用 then 方法时出现错误,则当前的 Promise 对象会被拒绝并抛出这个错误。
例如:
new Promise((resolve, reject) => {
resolve(thenable);
});
大致相当于:
new Promise((resolve, reject) => {
try {
thenable.then(
(value) => resolve(value),
(reason) => reject(reason),
);
} catch (e) {
reject(e);
}
});
但是在 resolve(thenable) 的情况中,有如下区别:
-
resolve函数是同步调用的,因此再次调用resolve或reject函数没有任何影响,即使通过anotherPromise.then()绑定的处理程序尚未被调用。
then 方法是异步调用的,因此如果传入thenable对象,则该 Promise 对象不会被立即解决。 -
因为 resolve 函数再次调用时使用
thenable.then()传递给它的任何值作为value参数,所以解决函数能够展开嵌套的 thenable 对象,其中一个 thenable 对象调用其onFulfilled处理程序并返回另一个 thenable 对象。这样做的效果是,真实的 Promise 对象的兑现处理器永远不会接收到 thenable 对象作为其兑现值。
Promise.resolve()
Promise.resolve() 静态方法将给定的值转换为一个 Promise。
- 如果该值本身就是一个 Promise,那么该 Promise 将被返回;
- 如果该值是一个 Thenable 对象,
Promise.resolve()将调用其then()方法及其两个回调函数; - 否则,返回的 Promise 将会以该值兑现,相当于
new Promise((resolve) => resolve(value))的简写形式。
通常,如果你不知道一个值是否是 Promise,那么最好使用 Promise.resolve(value) 将其转换成 Promise 对象,并将返回值作为 Promise 来处理。
例如,使用原 Promise:
const promise1 = Promise.reject(123);
const promise2 = Promise.resolve(promise1); // 没有创建新的 Promise
console.log(promise1 === promise2); // true
例如,创建一个以指定值兑现的 Promise:
const promise1 = Promise.resolve(123);
// 此时,promise1 已经敲定了 123
promise1.then((value) => {
console.log(value);
// Expected output: 123
});
例如,使用 Thenable 对象创建一个 Promise:
const aThenable = {
then(onFulfilled, onRejected) {
onFulfilled(42);
},
};
Promise.resolve(aThenable); // 一个兑现值为 42 的 Promise
展平嵌套类 Promise 对象
Promise.resolve() 会将嵌套的类 Promise 对象(例如,一个将被兑现为另一个 Promise 对象的 Promise 对象)展平,转化为单个 Promise 对象,其兑现值为一个 非 Thenable 值。
也就是说,如果函数调用 then 解决一次后,onFulfilled 接收到的参数是另一个 Thenable 对象,则会再次进行解决,以确保 Promise 对象的最终兑现值永远不会是 Thenable 对象。
const aThenable = {
then(onFulfilled, onRejected) {
onFulfilled({
// thenable 对象被兑现为另一个 thenable 对象
then(onFulfilled, onRejected) {
onFulfilled(42);
},
});
},
};
Promise.resolve(aThenable); // 一个兑现值为 42 的 Promise
Promise.reject()
Promise.reject() 返回一个新的 Promise 对象,该对象以给定的原因拒绝。
Promise.reject() 实际上相当于 new Promise((resolve, reject) => reject(reason)) 的简写形式。
Promise.reject(new Error("失败")).then(
() => {
// 不会被调用
},
(error) => {
console.error(error); // Stacktrace
},
);
Reject 一个 Promise 对象
与 Promise.resolve() 不同,即使 reason 已经是一个 Promise 对象,Promise.reject() 方法也始终会将其封装在一个新的 Promise 对象中。
const p = Promise.resolve(1);
const rejected = Promise.reject(p);
console.log(rejected === p); // false
rejected.catch((v) => {
console.log(v === p); // true
});
在非 Promise 构造函数上调用 reject()
Promise.reject() 方法是一个通用方法。它可以在任何实现与 Promise() 构造函数相同签名的构造函数上调用。
例如,我们可以在一个构造函数上调用它,并传入 console.log 作为 reject 参数:
class NotPromise {
constructor(executor) {
// “resolve”和“reject”函数的行为与原生 Promise 完全不同,但 `Promise.reject()` 方法以相同的方式调用它们。
executor(
(value) => console.log("已解决", value),
(reason) => console.log("已拒绝", reason),
);
}
}
Promise.reject.call(NotPromise, "foo"); // 输出 "已拒绝 foo"
Promise.withResolvers()
Promise.withResolvers() 静态方法返回一个对象,其包含一个新的 Promise 对象和两个函数,用于解决或拒绝它,对应于传入给 Promise() 构造函数执行器的两个参数。
let { promise, resolve, reject } = Promise.withResolvers();
Promise.withResolvers() 完全等同于以下代码:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
使用 Promise.withResolvers() 关键的区别在于解决和拒绝函数现在与 Promise 本身处于同一作用域,而不是在执行器中被创建和一次性使用。这可能使得一些更高级的用例成为可能,例如在重复事件中重用它们,特别是在处理流和队列时。这通常也意味着相比在执行器内包装大量逻辑,嵌套会更少。
参考
Promise
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
如何使用 Promise
https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous/Promises
Promise.resolve()
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
Promise.reject()
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject