Async 函数是一个非常了不起的东西,它将会在Chrome 55中得到默认支持。它允许你书写基于promise的代码,但它看起来就跟同步的代码一样,而且不会阻塞主线程。所以,它让你的异步代码看起来并没有那么"聪明"却更具有可读性。
Async 函数的代码示例:
- async function myFirstAsyncFunction() {
- try {
- const fulfilledValue = await promise;
- }
- catch (rejectedValue) {
- // …
- }
- }
如果你在一个函数声明的的前面使用async关键字,那你就可以在这个函数内使用await。当你去await一个promise的时候,这个函数将会以非阻塞的方式暂停,直到promise处于settled状态。如果这个Promise返回的是成功的状态,你将会得到返回值,如果返回的是失败的状态,那失败的信息将会被抛出。
提示: 如果你对promises不熟悉,请查看我们的promises指南
示例1: 打印响应信息
假设我们想要请求一个URL然后把响应信息打印出来,下面是使用promise的示例代码:
- function logFetch(url) {
- return fetch(url)
- .then(response => response.text())
- .then(text => {
- console.log(text);
- }).catch(err => {
- console.error('fetch failed', err);
- });
- }
下面用async 函数来实现同样的功能:
- async function logFetch(url) {
- try {
- const response = await fetch(url);
- console.log(await response.text());
- }
- catch (err) {
- console.log('fetch failed', err);
- }
- }
可以看到代码行数和上例一样,但是使用async函数的方式使得所有的回调函数都不见了!这让我们的代码非常容易阅读,特别是那些对promise不是特别熟悉的同学。
提示: 你await的任何值都是通过Promise.resolve()来传递的,所以你可以安全地使用非本地的promise.
Async 函数的返回值
不管你是否在函数内部使用了await, Async 函数总是返回一个promise 。当 async函数显示滴返回任意值时,返回的promise将会调用resolve方法, 当async函数抛出异常错误时,返回的promise将会调用reject方法,所以:
- // wait ms milliseconds
- function wait(ms) {
- return new Promise(r => setTimeout(r, ms));
- }
- async function hello() {
- await wait(500);
- return 'world';
- }
当执行hello()时,返回一个成功状态,并且传递的值为world的promise.
- async function foo() {
- await wait(500);
- throw Error('bar');
- }
当执行hello()时,返回一个失败状态,并且传递的值为Error('bar')的promise.
示例2: 响应流
在更复杂点的案例中, async函数更能体现其优越性。假设我们想要在记录chunks数据时将其变成响应流, 并返回最终的信息长度。
提示: "记录chunks" 让我感觉很别扭.
下面是使用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 并建立异步循环的了吗?这样写让我觉得自己“很聪明”。但是正如大多数“聪明的”代码一样,你不得不盯着它看很久才能搞清楚它在做什么,就像九十年代的那些魔眼照片一样。引用
让我们用async函数来重写上面的功能:
- 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循环来代替,这使我感觉非常的整洁。更多的是,在将来,我们将会使用async iterators,它将会使用for of循环来代替while循环,那这讲会变得更加整洁!
提示: 我对streams比较有好感。如果你对streams不太熟悉,可以看看我的指南
Async 函数的其他语法
我们已经看过了async function() {} 的使用方式,但是async关键字还可以用于其他的函数语法中。
箭头函数
- // map some URLs to json-promises
- const jsonPromises = urls.map(async url => {
- const response = await fetch(url);
- return response.json();
- });
提示: array.map(func)不会在乎你给的是否是async函数,它只会把它当做一个返回值是promise的普通函数。所以,第二个回调的执行并不会等待***个回调中的await处理完成。
对象方法
- 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(…);
提示: 类的 constructors和getters/settings不能是 async 函数。
注意!请避免太过强调顺序
尽管你正在写的代码看起来是同步的,但请确保你没有错失并行处理的机会。
- async function series() {
- await wait(500);
- await wait(500);
- return "done!";
- }
上面的代码需要 1000ms才能完成,然而:
- async function parallel() {
- const wait1 = wait(500);
- const wait2 = wait(500);
- await wait1;
- await wait2;
- return "done!";
- }
上面的代码只需要500ms,因为两个wait在同一时间处理了。
示例3: 顺序输出请求信息
假设我们想要获取一系列的URL响应信息,并将它们尽可能快的按正确的顺序打印出来。
深呼吸....下面就是使用promise来实现的代码:
- function logInOrder(urls) {
- // fetch all the URLs
- const textPromises = urls.map(url => {
- return fetch(url).then(response => response.text());
- });
- // log them in order
- textPromises.reduce((chain, textPromise) => {
- return chain.then(() => textPromise)
- .then(text => console.log(text));
- }, Promise.resolve());
- }
Yeah, 这达到了目的。我正在用reduce来处理一串的promise,我太"聪明"了。这是一个如此"聪明"的代码,但我们***不要这样做。
但是,当把上面的代码转换成使用 async函数来实现时,它看起来太有顺序了,以至于会使我们很迷惑:
:-1: 不推荐 - 过于强调先后顺序
- async function logInOrder(urls) {
- for (const url of urls) {
- const response = await fetch(url);
- console.log(await response.text());
- }
- }
看起来整洁多了,但是我的第二个请求只有在***个请求被完全处理完成之后才会发出去,以此类推。这个比上面那个promise的实例慢多了。幸好这还有一个中立的方案:
:+1: 推荐 - 很好而且并行
- async function logInOrder(urls) {
- // fetch all the URLs in parallel
- const textPromises = urls.map(async url => {
- const response = await fetch(url);
- return response.text();
- });
- // log them in sequence
- for (const textPromise of textPromises) {
- console.log(await textPromise);
- }
- }
在这个例子中,全部的url一个接一个被请求和处理,但是那个'聪明的'的reduce被标准的,普通的和更具可读性的for loop 循环取代了。
浏览器兼容性和解决方法
在我写这篇文章时,Chrome 55已经默认支持async 函数。但是在所有主流浏览器中,它还在开发中:
- Edge - In build 14342+ behind a flag
- Firefox - active development
- Safari - active development
解决方法 1:Generators
所有的主流浏览器的***版本都支持generators,如果你正在使用它们,你可以稍稍polyfill一下 async函数.
Babel正可以为你做这些事情,这里有个通过Babel REPL写的示例 - 是不是感觉对转换后的代码很熟悉。这个转换机制是 Babel's es2017 preset的一部分。
提示: Babel REPL是一个很有趣的东西,试试吧。
我建议你现在就这样做,因为当你的目标浏览器支持了async函数时,你只需要将Babel从你的项目中去除即可。但是如果你真的不想使用转换工具,你可以使用Babel's polyfill点击预览。
- async function slowEcho(val) {
- await wait(1000);
- return val;
- }
当你使用了上面说的polyfill点击预览,你可以将上面的代码替换为:
- const slowEcho = createAsyncFunction(function*(val) {
- yield wait(1000);
- return val;
- });
注意到你通过给createAsyncFunction函数传递了一个generator (function*),然后使用yield 代替 await。除此之外它们的效果一样。
解决方法2: regenerator
如果你想要兼容旧的浏览器,Babel同样也能把generators给转换了,这样你就可以在IE8以上的浏览器中使用async函数,但你需要使用Babel的 es2017 preset和 the es2015 preset
你会看到转换后的代码并不好看,所以请小心代码膨胀。
Async all the things!
一旦所有浏览器都支持async函数了,请在所有返回值是promise的函数上使用async!因为它不仅可以使你的代码更tider, 而且它确保了async函数 总是返回一个 promise 。
回到 2014 年,我对async函数的出现感到非常激动, 现在很高兴看到它们在浏览器中被支持了。Whoop!