前言
这篇文章是异步发展流程系列的最后一篇,可能会涉及
Promise
、Generators
、co
等前置知识,如果对这些不是很了解可以看这个系列的前三篇:如果已经具备这些前置知识,那我们继续看看今天的主角,
JavaScript
异步编程的终极大招async/await
。
async/await 简介
async/await
指的是两个关键字,是ES7
引入的新标准,async
关键字用于声明async
函数,await
关键字用来等待异步(必须是Promise
实例)操作,说白了async/await
就是Generators + co
的语法糖。
async/await
和 Generators + co
的写法非常的相似,只是把用于声明 Generator
函数的 *
关键字替换成了 async
并写在了 function
关键字的前面,把 yield
关键字替换成了 await
;另外,async
函数是基于 Promise
的,await
关键字后面等待的异步操作必须是一个 Promise
实例或基本类型的值,基本类型值的执行效果等同于同步,与 Generator
不同的是,await
关键字前可以使用变量去接收这个正在等待的 Promise
实例执行后的结果。
async 函数的基本用法
async
函数返回一个Promise
实例,可以使用then
方法添加回调函数。当函数执行的时候,只要遇到await
就会等待,直到await
后面的同步或异步操作完成,再接着执行函数体内后面的语句。
async 函数声明
async
的声明方式大概有以下几种:
/* async 函数声明 */
// 函数声明
async function fn() {}
// 函数表达式
const fn = async function () {};
// 箭头函数
const fn = async () => {};
// 作为对象的方法
const obj = {
async fn() {}
};
// 作为 class 的方法
class Person(name) {
constructor () {
this.name = name;
}
async getName() {
const name = await this.name;
return name;
}
}
在上一篇介绍 Generators + co
的文章中我们举了一个例子,使用 Node.js
的 fs
模块连续异步读文件,第一个文件名为 a.txt
,读到的内容为 b.txt
,作为要读的第二个文件的文件名,继续读 b.txt
后将读到的内容 “Hello world” 打印出来。
我们来使用 async/await
的方式来实现一下:
/* async 函数实现文件读取 */
// 引入依赖
const fs = require('fs');
const util = require('util');
// 将 fs.readFile 转换成 Promise
const readFile = util.promisify(fs.readFile);
// 声明 async 函数
async function read(file) {
const aData = await readFile(file, 'utf8');
const bData = await readFile(aData, 'utf8');
return bData;
}
// 调用 async 函数
read('a.txt').then(data => {
console.log(data); // Hello world
});
其实对比上一篇文章 Generator
的案例,与 Generator
函数一样,写法像同步,执行是异步,不同的是我们即没有手动调用 next
方法,也没有借助 co
库,其实是 async
函数内部集成了类似于 co
的执行器,帮我们在异步完成后自动向下执行代码,所以说 async/await
是 Generators + co
的语法糖。
async 函数错误处理
async
函数内部如果执行错误可以有三种方式进行错误处理:
- 在
await
后面的Promise
实例使用then
方法错误回调或catch
方法进行错误处理;- 如果有多个
await
,可以在async
函数执行完后使用catch
方法统一处理;- 由于
async
内部代码是同步的写法,多个await
的情况也可以使用try...catch...
进行处理。
需要注意的是,如果在
async
函数内部使用了try...catch...
又在函数执行完后使用了catch
,错误会优先被同步的try...catch...
捕获到,后面的catch
就不会再捕获了。
/* async 函数异常捕获 */
// 第一种
async function fn() {
const result = await Promise.reject('error').catch(err => {
console.log(err);
});
}
fn(); // error
// 第二种
async function fn() {
try {
const val1 = await Promise.reject('error');
const val2 = await Promise.resolve('success');
} catch (e) {
console.log(e);
}
}
fn(); // error
// 第三种
async function fn() {
const val1 = await Promise.resolve('success');
const val2 = await Promise.reject('error');
}
fn().catch((err => console.log(err))); // error
await 异步并发
在 async
函数中,如果有多个 await
互不依赖,这种情况下如果执行一个,等待一个完成,再执行一个,再等待完成,这样是很浪费性能的,所以我们要把这些异步操作同时触发。
假设我们异步读取两个文件,且这两个文件不相关,我可以使用下面的方式来实现:
/* await 异步并发 */
// 前置
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
// 需要改进的 async 函数
async function fn() {
const aData = await readFile('a.txt', 'utf8');
const bData = await readFile('b.txt', 'utf8');
return [aData, bData];
}
fn();
// 在 async 函数外部触发异步
const aDataPromise = readFile('a.txt', 'utf8');
const bDataPromise = readFile('b.txt', 'utf8');
async function fn() {
const aData = await aDataPromise;
const bData = await bDataPromise;
return [aData, bData];
}
fn();
// 使用 Promise.all
async function fn() {
const dataArr = await Promise.all(
readFile('a.txt', 'utf8'),
readFile('a.txt', 'utf8')
);
return dataArr;
}
fn();
使用 async/await 的注意点
使用
async/await
应注意以下几点:
- 对
await
习惯性错误处理;await
命令后互不依赖的异步应同时触发;async
函数中,函数的执行上/下文发生变化时,不能使用await
(如使用forEach
循环的回调中)。
针对第一点,在 async
函数中 await
命令后面大多情况下是 Promise
异步操作,运行结果可能出现错误并调用 reject
函数,最好对这个 await
语句进行错误处理,具体方式参照 async
函数基本用法中关于错误处理的内容。
针对第二点,如果两个或多个 await
命令后的异步操作没有依赖关系,执行时,需先触发第一个,等待异步完成,再触发第二个,再等异步完成,依次类推,这样比较耗时,性能不好,所以应该将这些异步操作同时触发,触发方式参照 async
函数基本用法中的 await
异步并发的内容。
针对第三点,如果声明一个 async
函数并传入一个数组,数组里面存储的都是 Promise
实例,若使用 forEach
循环数组,由于函数的执行上/下文发生了变化,此时使用 await
命令会报错。
/* 循环内使用 await */
// 创建 Promise 实例
const p1 = Promise.resolve('p1 success');
const p2 = Promise.resolve('p2 success');
const p3 = Promise.resolve('p3 success');
// async 函数
async function fn(promises) {
promise.forEach(function (promise) {
await promise;
});
}
fn([p1, p2, p3]); // 执行时报错
// 修改方式
async function fn(promises) {
for (let i = 0; i < promises.length; i++) {
await pormises[i];
}
}
fn([p1, p2, p3]); // 正常执行
总结
async/await
的实现原理,其实就是在 async
函数内部逻辑映射成了 Generator
函数并集成了一个类似于 co
的执行器,所以我们使用 async/await
的时候,代码更简洁,没有了自己触发遍历器的 next
或调用 co
充当执行器的过程,只需要关心 async
函数的内部逻辑就可以了,因为写法与同步相同,更提高了代码的可读性,所以说 async/await
是异步编程的终极大招。
由于
async/await
是ES7
规范,在浏览器端的支持并不是那么的友好,所以现在这种写法多用在Node.js
的异步操作当中,在Node.js
框架Koa 2.x
版本得到广泛应用。
最后希望大家在读过异步发展流程这个系列之后,对
JavaScript
异步已经有了较深的认识,并可以在不同情况下游刃有余的使用这些处理异步的编程手段。