我保证你很快就能理解 JavaScript 中的异步
当你探索现代 JavaScript 时,很快就会遇到主要的异步关键字之一:Promise
、await
或async
。那么,它们是如何工作的?你为什么要使用它们?(最后,还有一些充分利用它们的专业技巧。)
与异步编程中的所有事物一样,我们最终会回答这些问题,但我们回答的顺序尚未确定。
async function writeBlogPost() {
await Promise.all([
writeHowAsyncWorks(),
writeWhyAsync().then(() => writeAsyncIsNotMultithreading())
])
.then(() => writeProTips())
.finally(() => writeConclusion());
}
为什么是异步?
JavaScript 自诞生之日起就存在于互联网中。这意味着它必须处理一些耗时不确定的任务(通常是从你的设备向某个服务器发出的调用)。JavaScript 传统上处理这类任务的方式是“回调”:
function getImageAndDoSomething() {
// This is a simplified example, of course, since arrow functions
// didn't exist back in the day...
loadDataFromSite(
// Function argument 1: a URL
"http://placekitten.com/200/300",
// Function argument 2: a callback
(image, error) => {
// Do something with `image`
}
);
}
回调函数是指在工作完成后调用的函数。上面的函数会在成功从目标 URL 加载数据后,loadDataFromSite
调用带有定义参数的回调函数。如果加载失败,则会调用带有定义参数的回调函数,并将图像设置为定义参数。image
null
error
当你处理简单的“获取并执行一件事”循环时,这种方法没问题。但是,如果你需要对服务器进行多次链式调用,这种方法很快就会陷入回调地狱:
function apiCallbackHell() {
loadData((data, error) => {
data && transformData(data, (transformed, error) => {
transformed && collateData(transformed, (collated, error) => {
collated && discombobulateData(collated, (discombobulated, error) => {
// And so on...
})
})
})
})
}
这真是一团糟!像这样的回调地狱正是Promise API背后的动机,而 Promise API 又催生了async/await API。稍后我们会详细分析它的作用,但现在我们先来欣赏一下使用 async/await 后函数看起来多么简洁:
async function notApiCallbackHell() {
const data = await loadData();
const transformed = await transformData(data);
const collated = await collateData(transformed);
const discombobulated = await discombobulateData(collated);
// And so on...
}
支线任务:异步不是多线程 JavaScript
不过,在我们深入分析之前,我们先来澄清一个常见的误解:异步代码与多线程代码不同。JavaScript 的核心仍然是单线程环境。
该语言的底层有一个叫做“事件循环”的东西,它是负责读取并执行单个指令的引擎。该循环仍然是一个单线程进程——它每次只能读取一条指令,然后继续执行。
回调和 Promise 让这个循环看起来像是在同时执行多件事,但事实并非如此。我们可以把代码中的指令想象成一堆牌,而事件循环就像一个发牌人,每次从最上面抽出一张牌,然后把它们叠成一副整齐的牌。如果没有回调或 Promise,那么发牌人能抽出的牌堆就很明确了:它就是程序里的东西,从上到下逐行读取代码。
添加异步代码后,我们的发牌人可以从另一个堆中读取数据——回调或 Promise 中的代码可以独立于程序全局范围内的指令读取。然而,仍然只有一个发牌人(一个线程),并且他们一次只能读取一条指令。只不过现在他们的工作在不同的堆中共享了。这意味着,如果你把一些非常困难的工作放入 Promise 中,你将创建一个非常大的新堆供你的发牌人读取数据。这会减慢其他代码的执行速度,因此屏幕上的交互式 UI 可能会变得非常慢。
解决这个问题的方法是将你的繁重工作转移到另一个线程——用我们的比喻来说,这就像是雇佣第二个经销商,独立于主经销商,对堆积如山的指令进行整理。如何做到这一点超出了本文的讨论范围,但如果你对此感兴趣,可以查看Node 的工作线程或浏览器的 Web Workers。
这里有哪些碎片?
因此,我们已经听说过 async/await 领域中的三种主要工具,但它们实际上做什么以及它们如何工作?
承诺
async/await 工具包的核心是Promise
类型。s是对象。它们包装执行某些Promise
操作的代码。其最初的目的是为了更容易地将回调和错误处理程序附加到该代码。创建 Promise 的方法有很多种,但最基本的是:
new Promise((resolve, reject) => {
// Do something
if (itSucceeded) {
resolve(successResult);
} else {
reject(failureReason);
}
});
在这里你可以看到 a 的核心特性Promise
——它只是一个回调的包装器!在我们 new 的执行块中,Promise
我们只有两个回调——一个是 Promise 成功执行时调用的回调(resolve
回调),另一个是 Promise 失败时调用的回调(reject
回调)。
Promise
然后我们得到两个最重要的函数:
const somePromise = getPromise();
somePromise
.then((result) => {
// Do something with a success
})
.catch((rejection) => {
// Do something with a rejection
});
then
如果您从其他代码中catch
获得了 ,则它们非常有用。您可以通过这些方法将自己的回调附加到,以监听其解析时间(在这种情况下,您的回调将使用解析值进行调用)或处理失败情况(在这种情况下,您的回调将使用拒绝原因(如果有)进行调用)。Promise
Promise
then
catch
(附注:finally
正如您可能猜到的那样,在所有then
和catch
处理程序完成后,还会运行一个。)
Then 和 catch 也很有用,因为它们本身返回一个Promise
包含处理程序返回值的 now。
因此,您可以使用.then
将多个步骤链接在一起,部分逃避回调地狱:
function promisePurgatory() {
loadData(data)
.then(data => transformData(data))
.then(transformed => collateData(transformed))
.then(collated => discombobulateData(collated))
.then( /* and so on */ );
}
异步/等待
不过,你可能已经注意到,这Promise
并不能完全让我们摆脱对大量回调的需求。当然,它们现在都在同一层级,所以我们不再需要无限循环。但是,JavaScript 背后的社区确信他们可以做得更好。Enterasync
及其合作伙伴await
。这两个工具Promise
极大地简化了编程。
首先是async
- 这是一个关键字,用来注释一个函数,表示它返回一个Promise
。你无需做任何进一步的操作,如果你将一个函数标记为async
,它现在将被视为与你将其作为 Promise 中的执行块一样。
async function doSomeWork() {
// Do some complicated work and then
return 42;
}
async function alwaysThrows() {
// Oh no this function always throws
throw "It was called alwaysThrows, what did you expect?"
}
const automaticPromise = doSomeWork();
// Without having to call `new Promise` we have one.
// This will log 42:
automaticPromise.then((result) => console.log(result));
const automaticReject = alwaysThrows();
// Even though the function throws, because it's async the throw
// is wrapped up in a Promise reject and our code doesn't crash:
automaticReject.catch((reason) => console.error(reason));
这本身就很有用——你再也不用记住如何实例化,Promise
也不用担心处理大小写reject
和throw
错误了。但真正精彩的是当你添加 时await
。
await
只能存在于async
函数内部,但它提供了一种暂停函数的方法,直到其他函数Promise
完成。然后,您将获得该函数的解析值Promise
,或者,如果它被拒绝,则会抛出拒绝信息。这允许您Promise
直接处理结果,而无需为其构建回调。这是我们真正摆脱回调地狱所需的最后一个工具:
// From above, now with error handling
async function notApiCallbackHell() {
try {
const data = await loadData();
const transformed = await transformData(data);
const collated = await collateData(transformed);
const discombobulated = await discombobulateData(collated);
// And so on...
} catch {
// Remember - if the Promise rejects, await will just throw.
console.error("One of our ladders out of hell failed");
}
}
一些 Pro(mise) 技巧
Promise
现在您已经对、async
和 的基础知识有了await
更好的了解,下面是使用它们时需要牢记的一些专业提示:
-
async
和.then
会Promise
自动展平返回的 s。和 都async
足够.then
智能,知道如果您Promise
为某个值返回 a ,您的最终用户并不想为某个值返回 aPromise
。Promise
您可以直接返回您的值,也可以Promise
为其返回 a ,它都会被正确地展平。 -
Promise.all
用于连接,而不是多个await
s。如果你有多个Promise
s,它们彼此不依赖,并且你想等待它们全部完成,你的第一反应可能是这样做:
async function waitForAll() {
// Don't do this
const one = await doPromiseOne();
const two = await doPromiseTwo();
const three = await doPromiseThree();
}
不过,这会给你带来麻烦,因为你必须等待第一个 Promise 完成后才能开始第二个 Promise,以此类推。你应该使用内置函数Promise.all
:
async function waitForAll() {
const [one, two, three] = await Promise.all([
doPromiseOne(), doPromiseTwo(), doPromiseThree()
]);
}
这样,您的代码将预先创建所有三个 Promise,并同时执行它们。您仍然需要完成await
所有三个 Promise,但所需时间会大大减少,因为您可以将 PromiseOne 的停机时间用于处理 PromiseTwo 或 Three。
-
Promise.allSettled
如果失败是可以接受的。Promise.all
序列化s的缺点await
是,如果其中一个Promise
s 被拒绝,那么整个链都会被拒绝。这就是它的Promise.allSettled
用武之地。它的工作原理与 s 相同,Promise.all
只是它会等到所有参数都解析或拒绝后,再将 s 本身的数组返回给你Promise
。如果你正在尝试做一些工作,但即使失败也没关系,这很有用。 -
箭头函数也可以
async
。最后但同样重要的是,请务必记住,箭头函数async
也可以标记为!如果您要创建一个需要使用 的回调处理程序await
,例如用于onSubmit
表单的 ,这真的非常有用:
// Imagining we're in react...
return <Form onSubmit={
async (values) => {
const serverResponse = await submitValuesToServer(values);
window.location.href = "/submitted/success";
}
}>{/* Form contents */}</Form>
。最后(...)
在下面的评论中告诉我你现在对Promise
、async
和有什么疑问await
。尽管我在编写的每个 Node 和 React 应用中都使用了这三个,但它们之间仍然有很多细微的差别需要学习。
如果您喜欢这篇文章,请给我点个赞,或者可以看看我上一篇关于JS细节的this
“回归基础”文章。