异步函数可让您像编写同步代码那样编写基于 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')
时拒绝。
示例:流式传输响应
异步函数在更复杂示例中更有用武之地。假设您想在流式传输响应的同时记录数据块日志,并返回数据块最终大小。
以下是使用 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);
});
});
}
请“Promise 大师”Jake Archibald 给我检查一下。看到我是如何在 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 毫秒就可执行完毕,因为两个 wait 是同时发生的。 我们来看一个实际示例。
示例:按顺序输出提取的内容
假设您想获取一系列网址,并尽快按正确顺序将它们记录到日志中。
深呼吸 - 以下是使用 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); } }
浏览器支持权宜解决方案:生成器
如果目标是支持生成器的浏览器(其中包括每一个主流浏览器的最新版本),可以通过 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
。其他方面的工作方式是相同的。
解决方法:再生器
如果目标是旧版浏览器,Babel 还可转译生成器,让您能在版本低至 IE8 的浏览器上使用异步函数。为此,您需要 Babel 的 es2017 预设 和 es2015 预设。
输出不够美观,因此要注意避免发生代码膨胀。
全面异步化!
一旦异步函数登陆所有浏览器,就在每一个返回 Promise 的函数上尽情使用吧!它们不但能让代码更加整洁美观,还能确保该函数始终都会返回 Promise。
我真正热衷于使用异步函数的历史可以追溯到 2014 年,看到它们登陆浏览器即将成真,真是棒极了。好耶!