借助异步函数,您可以编写基于 Promise 的代码,就像它是同步代码一样。
异步函数在 Chrome、Edge、Firefox 和 Safari 中默认处于启用状态,而且效果非常出色。借助它们,您可以编写基于 Promise 的代码,就像它是同步代码一样,但不会阻塞主线程。它们可使异步代码更易于阅读,并减少“巧妙”的代码。
异步函数的运作方式如下:
async function myFirstAsyncFunction() { try { const fulfilledValue = await promise; } catch (rejectedValue) { // … } }
如果您在函数定义之前使用 async
关键字,则可以在函数内使用 await
。当您 await
一个 promise 时,该函数会以非阻塞方式暂停,直到 promise 得到解决。如果 promise 执行完毕,您将会收到相应值。如果 promise 被拒绝,则会抛出被拒绝的值。
浏览器支持
示例:记录提取操作
假设您要提取网址并将响应记录为文本。使用 promise 时,代码如下所示:
function logFetch(url) { return fetch(url) .then((response) => response.text()) .then((text) => { console.log(text); }) .catch((err) => { console.error('fetch failed', err); }); }
下面是使用异步函数实现相同操作的代码:
async function logFetch(url) { try { const response = await fetch(url); console.log(await response.text()); } catch (err) { console.log('fetch failed', err); } }
行数相同,但所有回调都消失了。这样一来,代码就更容易阅读了,对于不太熟悉 Promise 的用户来说更是如此。
异步返回值
无论您是否使用 await
,异步函数都始终返回 Promise。该 Promise 会使用异步函数返回的任何内容进行解析,或使用异步函数抛出的任何内容进行拒绝。因此,使用以下命令:
// wait ms milliseconds function wait(ms) { return new Promise((r) => setTimeout(r, ms)); } async function hello() { await wait(500); return 'world'; }
…调用 hello()
会返回一个 promise,该 promise 会使用 "world"
执行。
async function foo() { await wait(500); throw Error('bar'); }
…调用 foo()
会返回一个 promise,该 promise 会使用 Error('bar')
rejects。
示例:流式传输响应
在更复杂的示例中,异步函数的好处会更加明显。假设您希望在记录分块的同时流式传输响应,并返回最终大小。
下面是使用 promise 的版本:
function getResponseSize(url) { return fetch(url).then((response) => { const reader = response.body.getReader(); let total = 0; return reader.read().then(function processResult(result) { if (result.done) return total; const value = result.value; total += value.length; console.log('Received chunk', value); return reader.read().then(processResult); }); }); }
我是“承诺大使”Jake Archibald。您看到了吗?我如何在 processResult()
自身内调用 processResult()
来设置异步循环?写作让我感觉很聪明。但与大多数“智能”代码一样,您必须盯着它看很长时间才能弄清楚它在做什么,就像 90 年代的魔眼图案一样。
我们再使用异步函数试一试:
async function getResponseSize(url) { const response = await fetch(url); const reader = response.body.getReader(); let result = await reader.read(); let total = 0; while (!result.done) { const value = result.value; total += value.length; console.log('Received chunk', value); // get the next result result = await reader.read(); } return total; }
所有“智能”功能都已消失。让我如此得意的异步循环被替换成了可靠但乏味的 while 循环。现在好多了。未来,您将获得异步迭代器,它会将 while
循环替换为 for-of 循环,使其更加简洁。
其他异步函数语法
我已经向您展示了 async function() {}
,但 async
关键字可以与其他函数语法搭配使用:
箭头函数
// map some URLs to json-promises const jsonPromises = urls.map(async (url) => { const response = await fetch(url); return response.json(); });
对象方法
const storage = { async getAvatar(name) { const cache = await caches.open('avatars'); return cache.match(`/avatars/${name}.jpg`); } }; storage.getAvatar('jaffathecake').then(…);
类方法
class Storage { constructor() { this.cachePromise = caches.open('avatars'); } async getAvatar(name) { const cache = await this.cachePromise; return cache.match(`/avatars/${name}.jpg`); } } const storage = new Storage(); storage.getAvatar('jaffathecake').then(…);
小心!避免过于顺序
虽然您编写的代码看起来是同步的,但请确保您不会错失并行执行操作的机会。
async function series() { await wait(500); // Wait 500ms… await wait(500); // …then wait another 500ms. return 'done!'; }
上述操作需要 1000 毫秒才能完成,而:
async function parallel() { const wait1 = wait(500); // Start a 500ms timer asynchronously… const wait2 = wait(500); // …meaning this timer happens in parallel. await Promise.all([wait1, wait2]); // Wait for both timers in parallel. return 'done!'; }
上述操作需要 500 毫秒才能完成,因为这两个等待操作会同时发生。我们来看一个实际示例。
示例:按顺序输出提取内容
假设您想提取一系列网址,并尽快按正确的顺序将其记录下来。
深呼吸 - 使用 Promise 时,情况如下所示:
function markHandled(promise) { promise.catch(() => {}); return promise; } function logInOrder(urls) { // fetch all the URLs const textPromises = urls.map((url) => { return markHandled(fetch(url).then((response) => response.text())); }); // log them in order return textPromises.reduce((chain, textPromise) => { return chain.then(() => textPromise).then((text) => console.log(text)); }, Promise.resolve()); }
是的,没错,我使用 reduce
串联一系列 promise。我很聪明。不过,这种编码方式有些过于精巧,最好还是不要这样做。
但是,将上述代码转换为异步函数时,很容易陷入过于顺序的陷阱:
async function logInOrder(urls) { for (const url of urls) { const response = await fetch(url); console.log(await response.text()); } }
function markHandled(...promises) { Promise.allSettled(promises); } async function logInOrder(urls) { // fetch all the URLs in parallel const textPromises = urls.map(async (url) => { const response = await fetch(url); return response.text(); }); markHandled(...textPromises); // log them in sequence for (const textPromise of textPromises) { console.log(await textPromise); } }
reduce
位已替换为标准的、乏味的、可读的 for 循环。 浏览器支持权宜解决方案:生成器
如果您定位的是支持生成器的浏览器(包括所有主流浏览器的最新版本),则可以对异步函数进行类似的 polyfill。
Babel 会为您完成此操作,以下是通过 Babel REPL 的示例
- 请注意转译后的代码有多相似。此转换是 Babel 的 es2017 预设的一部分。
我建议使用转译方法,因为您只需在目标浏览器支持异步函数后关闭转译方法即可,但如果您真的不想使用转译器,可以使用 Babel 的 polyfill 自行使用。来替代:
async function slowEcho(val) { await wait(1000); return val; }
…您需要添加polyfill 并编写以下代码:
const slowEcho = createAsyncFunction(function* (val) { yield wait(1000); return val; });
请注意,您必须将生成器 (function*
) 传递给 createAsyncFunction
,并使用 yield
而非 await
。除此之外,其运作方式与原来相同。
权宜解决方法:regenerator
如果您以旧版浏览器为目标平台,Babel 还可以转译生成器,让您能够在 IE8 等旧版浏览器中使用异步函数。为此,您需要 Babel 的 es2017 预设 和 es2015 预设。
输出不太美观,因此请注意代码膨胀。
将所有操作都异步执行!
在所有浏览器都支持异步函数后,请在每个返回 Promise 的函数中使用它们!这不仅可以让代码更整洁,还可以确保函数始终返回一个 Promise。
早在 2014 年,我就对异步函数非常感兴趣,很高兴看到它们真的在浏览器中推出了。好耶!