使用 Promise
一个 Promise
是一个对象,表示异步操作的最终完成或失败。由于大多数人都是已创建 promise 的使用者,因此本指南将在解释如何创建 promise 之前解释如何使用返回的 promise。
从本质上讲,promise 是一个返回的对象,您可以将其附加回调,而不是将回调传递到函数中。想象一个函数 createAudioFileAsync()
,它根据配置记录和两个回调函数异步生成声音文件:一个在成功创建音频文件时调用,另一个在发生错误时调用。
以下是一些使用 createAudioFileAsync()
的代码
function successCallback(result) {
console.log(`Audio file ready at URL: ${result}`);
}
function failureCallback(error) {
console.error(`Error generating audio file: ${error}`);
}
createAudioFileAsync(audioSettings, successCallback, failureCallback);
如果 createAudioFileAsync()
重写为返回 promise,则会将回调附加到它而不是传递给它
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
此约定有几个优点。我们将逐一探讨。
链式调用
一个常见需求是依次执行两个或多个异步操作,其中每个后续操作在先前操作成功时开始,并使用先前步骤的结果。在过去,连续执行多个异步操作会导致经典的 回调地狱
doSomething(function (result) {
doSomethingElse(result, function (newResult) {
doThirdThing(newResult, function (finalResult) {
console.log(`Got the final result: ${finalResult}`);
}, failureCallback);
}, failureCallback);
}, failureCallback);
使用 promise,我们可以通过创建 promise 链来实现这一点。promise 的 API 设计使这一点非常棒,因为回调附加到返回的 promise 对象,而不是传递到函数中。
以下是神奇之处:then()
函数返回一个与原始 promise 不同的**新 promise**
const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);
第二个 promise (promise2
) 表示不仅 doSomething()
的完成,还表示您传入的 successCallback
或 failureCallback
的完成——它们可以是返回 promise 的其他异步函数。在这种情况下,添加到 promise2
的任何回调都会排在 successCallback
或 failureCallback
返回的 promise 后面。
注意:如果您想要一个可以使用的有效示例,可以使用以下模板创建任何返回 promise 的函数
function doSomething() {
return new Promise((resolve) => {
setTimeout(() => {
// Other things to do before completion of the promise
console.log("Did something");
// The fulfillment value of the promise
resolve("https://example.com/");
}, 200);
});
}
该实现将在下面的 围绕旧回调 API 创建 Promise 部分中讨论。
使用此模式,您可以创建更长的处理链,其中每个 promise 表示链中一个异步步骤的完成。此外,then
的参数是可选的,catch(failureCallback)
是 then(null, failureCallback)
的简写——因此,如果您的错误处理代码对所有步骤都相同,则可以将其附加到链的末尾
doSomething()
.then(function (result) {
return doSomethingElse(result);
})
.then(function (newResult) {
return doThirdThing(newResult);
})
.then(function (finalResult) {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
您可能会看到它用 箭头函数 表示
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);
注意:箭头函数表达式可以具有 隐式返回;因此,() => x
是 () => { return x; }
的简写。
doSomethingElse
和 doThirdThing
可以返回任何值——如果它们返回 promise,则首先等待该 promise 完成,然后下一个回调接收完成值,而不是 promise 本身。重要的是始终从 then
回调中返回 promise,即使 promise 始终解析为 undefined
。如果前一个处理程序启动了一个 promise 但没有返回它,则无法再跟踪其完成情况,并且该 promise 被称为“浮动”promise。
doSomething()
.then((url) => {
// Missing `return` keyword in front of fetch(url).
fetch(url);
})
.then((result) => {
// result is undefined, because nothing is returned from the previous
// handler. There's no way to know the return value of the fetch()
// call anymore, or whether it succeeded at all.
});
通过返回 fetch
调用的结果(这是一个 promise),我们可以同时跟踪其完成情况并在其完成时接收其值。
doSomething()
.then((url) => {
// `return` keyword added
return fetch(url);
})
.then((result) => {
// result is a Response object
});
如果存在竞争条件,浮动 promise 会更糟糕——如果最后一个处理程序的 promise 没有返回,则下一个 then
处理程序将过早调用,并且它读取的任何值可能不完整。
const listOfIngredients = [];
doSomething()
.then((url) => {
// Missing `return` keyword in front of fetch(url).
fetch(url)
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
});
})
.then(() => {
console.log(listOfIngredients);
// listOfIngredients will always be [], because the fetch request hasn't completed yet.
});
因此,根据经验法则,每当您的操作遇到 promise 时,请返回它并将它的处理延迟到下一个 then
处理程序。
const listOfIngredients = [];
doSomething()
.then((url) => {
// `return` keyword now included in front of fetch call.
return fetch(url)
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
});
})
.then(() => {
console.log(listOfIngredients);
// listOfIngredients will now contain data from fetch call.
});
更好的是,您可以将嵌套链展平为单个链,这更简单并使错误处理更容易。详细信息将在下面的 嵌套 部分中讨论。
doSomething()
.then((url) => fetch(url))
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
})
.then(() => {
console.log(listOfIngredients);
});
使用 async
/await
可以帮助您编写更直观的代码,并且类似于同步代码。以下是使用 async
/await
的相同示例
async function logIngredients() {
const url = await doSomething();
const res = await fetch(url);
const data = await res.json();
listOfIngredients.push(data);
console.log(listOfIngredients);
}
请注意,代码看起来与同步代码完全相同,除了 promise 前面的 await
关键字。唯一的权衡之一是,您可能会忘记 await
关键字,这只能在类型不匹配时(例如,尝试将 promise 用作值)修复。
async
/await
基于 promise——例如,doSomething()
与之前的函数相同,因此从 promise 更改为 async
/await
所需的重构最少。您可以在 异步函数 和 await
参考中阅读有关 async
/await
语法的更多信息。
注意:async
/await
具有与普通 promise 链相同的并发语义。一个异步函数中的 await
不会停止整个程序,只会停止依赖其值的那些部分,因此在 await
等待时,其他异步作业仍然可以运行。
错误处理
您可能还记得在早期的厄运金字塔中看到了三次 failureCallback
,而在 promise 链的末尾只看到了一次
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);
如果发生异常,浏览器将沿着链向下查找 .catch()
处理程序或 onRejected
。这在很大程度上模拟了同步代码的工作方式
try {
const result = syncDoSomething();
const newResult = syncDoSomethingElse(result);
const finalResult = syncDoThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch (error) {
failureCallback(error);
}
这种与异步代码的对称性最终体现在 async
/await
语法中
async function foo() {
try {
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch (error) {
failureCallback(error);
}
}
Promise 通过捕获所有错误(即使是抛出的异常和编程错误)解决了厄运金字塔回调中的一个基本缺陷。这对于异步操作的功能组合至关重要。现在,所有错误都由链末尾的 catch()
方法处理,您几乎不需要在不使用 async
/await
的情况下使用 try
/catch
。
嵌套
在上面涉及 listOfIngredients
的示例中,第一个示例在一个 then()
处理程序的返回值中嵌套了一个 promise 链,而第二个示例使用了一个完全平坦的链。简单的 promise 链最好保持平坦,不要嵌套,因为嵌套可能是粗心组合的结果。
嵌套是一种控制结构,用于限制 catch
语句的作用域。具体来说,嵌套的 catch
只捕获其作用域和以下范围内的错误,而不是链中较高范围外的错误。如果使用正确,这将使错误恢复更加精确
doSomethingCritical()
.then((result) =>
doSomethingOptional(result)
.then((optionalResult) => doSomethingExtraNice(optionalResult))
.catch((e) => {}),
) // Ignore if optional stuff fails; proceed.
.then(() => moreCriticalStuff())
.catch((e) => console.error(`Critical failure: ${e.message}`));
请注意,此处的可选步骤是嵌套的——嵌套不是由缩进引起的,而是由围绕步骤的外部 (
和 )
括号的位置引起的。
内部的错误静默catch
处理程序只捕获来自doSomethingOptional()
和doSomethingExtraNice()
的失败,之后代码继续执行moreCriticalStuff()
。重要的是,如果doSomethingCritical()
失败,它的错误只会由最终的(外部)catch
捕获,而不会被内部的catch
处理程序吞噬。
在async
/await
中,这段代码看起来像这样
async function main() {
try {
const result = await doSomethingCritical();
try {
const optionalResult = await doSomethingOptional(result);
await doSomethingExtraNice(optionalResult);
} catch (e) {
// Ignore failures in optional steps and proceed.
}
await moreCriticalStuff();
} catch (e) {
console.error(`Critical failure: ${e.message}`);
}
}
注意:如果你没有复杂的错误处理,你很可能不需要嵌套的then
处理程序。相反,使用扁平化的链,并将错误处理逻辑放在最后。
在catch之后链接
可以在失败(即catch
)之后进行链接,这对于即使在链中某个操作失败后也要执行新操作很有用。阅读以下示例
doSomething()
.then(() => {
throw new Error("Something failed");
console.log("Do this");
})
.catch(() => {
console.error("Do that");
})
.then(() => {
console.log("Do this, no matter what happened before");
});
这将输出以下文本
Initial Do that Do this, no matter what happened before
注意:“执行此操作”文本不会显示,因为“某些操作失败”错误导致了拒绝。
在async
/await
中,这段代码看起来像这样
async function main() {
try {
await doSomething();
throw new Error("Something failed");
console.log("Do this");
} catch (e) {
console.error("Do that");
}
console.log("Do this, no matter what happened before");
}
Promise 拒绝事件
如果 Promise 拒绝事件没有被任何处理程序处理,它会冒泡到调用栈的顶部,并且宿主需要将其显示出来。在 Web 上,每当 Promise 被拒绝时,都会向全局作用域发送两个事件之一(通常,要么是window
,要么是在 Web Worker 中使用时,是Worker
或其他基于 Worker 的接口)。这两个事件是
unhandledrejection
-
当 Promise 被拒绝但没有可用的拒绝处理程序时发送。
rejectionhandled
-
当为已导致
unhandledrejection
事件的已拒绝 Promise 附加处理程序时发送。
在这两种情况下,事件(类型为PromiseRejectionEvent
)都具有作为成员的promise
属性,该属性指示被拒绝的 Promise,以及一个reason
属性,该属性提供拒绝 Promise 的原因。
这些使得可以为 Promise 提供回退错误处理,以及帮助调试 Promise 管理方面的问题。这些处理程序在每个上下文中都是全局的,因此所有错误都会转到相同的事件处理程序,而不管来源如何。
在Node.js中,处理 Promise 拒绝的方式略有不同。您可以通过为 Node.js 的unhandledRejection
事件添加处理程序来捕获未处理的拒绝(注意名称中大写字母的不同),如下所示
process.on("unhandledRejection", (reason, promise) => {
// Add code here to examine the "promise" and "reason" values
});
对于 Node.js,要防止错误被记录到控制台(否则会发生的默认操作),添加该process.on()
侦听器就足够了;不需要浏览器运行时的preventDefault()
方法的等效项。
但是,如果您添加了该process.on
侦听器,但没有在其中添加处理已拒绝 Promise 的代码,它们将被丢弃并静默忽略。因此,理想情况下,您应该在该侦听器中添加代码以检查每个已拒绝的 Promise,并确保它不是由实际的代码错误引起的。
组合
有四个组合工具用于并发运行异步操作:Promise.all()
、Promise.allSettled()
、Promise.any()
和Promise.race()
。
我们可以同时启动操作并等待它们全部完成,如下所示
Promise.all([func1(), func2(), func3()]).then(([result1, result2, result3]) => {
// use result1, result2 and result3
});
如果数组中的某个 Promise 被拒绝,Promise.all()
会立即拒绝返回的 Promise 并中止其他操作。这可能会导致意外的状态或行为。Promise.allSettled()
是另一个组合工具,它确保所有操作都完成后再解析。
这些方法都并发运行 Promise——一系列 Promise 会同时启动,并且不会互相等待。可以使用一些巧妙的 JavaScript 进行顺序组合
[func1, func2, func3]
.reduce((p, f) => p.then(f), Promise.resolve())
.then((result3) => {
/* use result3 */
});
在这个例子中,我们reduce了一个异步函数数组到一个 Promise 链。上面的代码等价于
Promise.resolve()
.then(func1)
.then(func2)
.then(func3)
.then((result3) => {
/* use result3 */
});
这可以转换成一个可重用的 compose 函数,这在函数式编程中很常见
const applyAsync = (acc, val) => acc.then(val);
const composeAsync =
(...funcs) =>
(x) =>
funcs.reduce(applyAsync, Promise.resolve(x));
composeAsync()
函数接受任意数量的函数作为参数,并返回一个新函数,该函数接受一个初始值,该值将通过组合管道传递
const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);
顺序组合也可以使用 async/await 更简洁地完成
let result;
for (const f of [func1, func2, func3]) {
result = await f(result);
}
/* use last result (i.e. result3) */
但是,在顺序组合 Promise 之前,请考虑是否真的有必要——始终最好并发运行 Promise,这样它们就不会不必要地相互阻塞,除非一个 Promise 的执行依赖于另一个 Promise 的结果。
取消
Promise
本身没有用于取消的一级协议,但您可能能够直接取消底层的异步操作,通常使用AbortController
。
围绕旧回调 API 创建 Promise
可以使用其构造函数从头开始创建Promise
。这应该只用于包装旧的 API。
在一个理想的世界里,所有异步函数都应该已经返回 Promise。不幸的是,有些 API 仍然期望以旧的方式传入成功和/或失败回调。最明显的例子是setTimeout()
函数
setTimeout(() => saySomething("10 seconds passed"), 10 * 1000);
混合旧式回调和 Promise 会有问题。如果saySomething()
失败或包含编程错误,则没有任何内容可以捕获它。这是setTimeout
设计的固有特性。
幸运的是,我们可以将setTimeout
包装在一个 Promise 中。最佳实践是在尽可能低的级别包装接受回调的函数,然后永远不要再次直接调用它们
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
wait(10 * 1000)
.then(() => saySomething("10 seconds"))
.catch(failureCallback);
Promise 构造函数接受一个执行器函数,该函数允许我们手动解析或拒绝 Promise。由于setTimeout()
实际上不会失败,因此在这种情况下我们省略了reject。有关执行器函数如何工作的更多信息,请参阅Promise()
参考。
计时
最后,我们将深入了解更技术性的细节,即注册的回调何时被调用。
保证
在基于回调的 API 中,回调何时以及如何被调用取决于 API 实现者。例如,回调可以同步或异步调用
function doSomething(callback) {
if (Math.random() > 0.5) {
callback();
} else {
setTimeout(() => callback(), 1000);
}
}
强烈不建议采用上述设计,因为它会导致所谓的“Zalgo 状态”。在设计异步 API 的上下文中,这意味着回调在某些情况下同步调用,而在其他情况下异步调用,从而为调用者造成歧义。有关更多背景信息,请参阅文章设计异步 API,其中首次正式提出了该术语。这种 API 设计使得副作用难以分析
let value = 1;
doSomething(() => {
value = 2;
});
console.log(value); // 1 or 2?
另一方面,Promise 是一种控制反转的形式——API 实现者不控制回调何时被调用。相反,维护回调队列和决定何时调用回调的任务被委托给 Promise 实现,并且 API 用户和 API 开发人员都自动获得强大的语义保证,包括
- 使用
then()
添加的回调永远不会在JavaScript 事件循环的当前运行完成之前被调用。 - 即使在 Promise 表示的异步操作成功或失败之后添加了这些回调,它们也会被调用。
- 可以通过多次调用
then()
来添加多个回调。它们将按插入顺序一个接一个地被调用。
为了避免意外情况,传递给then()
的函数永远不会同步调用,即使对于已解析的 Promise 也是如此
Promise.resolve().then(() => console.log(2));
console.log(1);
// Logs: 1, 2
传入的函数不会立即运行,而是被放入微任务队列中,这意味着它稍后运行(仅在创建它的函数退出后以及 JavaScript 执行栈为空时),就在控制权返回到事件循环之前;也就是说,很快
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
wait(0).then(() => console.log(4));
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
console.log(1); // 1, 2, 3, 4
任务队列与微任务
Promise 回调作为微任务处理,而setTimeout()
回调作为任务队列处理。
const promise = new Promise((resolve, reject) => {
console.log("Promise callback");
resolve();
}).then((result) => {
console.log("Promise callback (.then)");
});
setTimeout(() => {
console.log("event-loop cycle: Promise (fulfilled)", promise);
}, 0);
console.log("Promise (pending)", promise);
上面的代码将输出
Promise callback Promise (pending) Promise {<pending>} Promise callback (.then) event-loop cycle: Promise (fulfilled) Promise {<fulfilled>}
有关更多详细信息,请参阅任务与微任务。
Promise 和任务冲突时
如果您遇到 Promise 和任务(例如事件或回调)以不可预测的顺序触发的情况,则可能需要使用微任务来检查状态或在有条件地创建 Promise 时平衡 Promise。
如果您认为微任务可以帮助解决此问题,请参阅微任务指南,以了解如何使用queueMicrotask()
将函数排队作为微任务。
另请参阅
Promise
async function
await
- Promises/A+ 规范
- 我们有一个关于 Promise 的问题 在 pouchdb.com 上 (2015)