含义
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
const asyncReadFile = async function() {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
2
3
4
5
6
async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。
async 函数对 Generator 函数的改进
- 内置执行器。
Generator 函数的执行必须靠执行器,所以才有了 co 模块,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
asyncReadFile();
- 更好的语义。
async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。 - 更广的适用性。
co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。 - 返回值是 Promise。
async 函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用 then 方法指定下一步的操作。
进一步说,async 函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖。
基本用法
async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name);
const stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function(result) {
console.log(result);
});
2
3
4
5
6
7
8
9
async 函数有多种使用形式。
// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// Class 的方法
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('jake').then(…);
// 箭头函数
const foo = async () => {};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
语法
返回 Promise 对象
async 函数返回一个 Promise 对象。
async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。
async function f() {
return 'hello world';
}
f().then(v => console.log(v));
// "hello world"
2
3
4
5
6
async 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态。抛出的错误对象会被 catch 方法回调函数接收到。
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出错了
2
3
4
5
6
7
8
9
返回的 Promise 对象的状态变化
async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。也就是说,只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数.
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log);
// "ECMAScript 2017 Language Specification"
2
3
4
5
6
7
上面代码中,函数 getTitle 内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行 then 方法里面的 console.log。
await 命令
正常情况下,await 命令后面是一个 Promise 对象。如果不是,就返回对应的值。
async function f() {
// 等同于
// return 123;
return await 123;
}
f().then(v => console.log(v));
// 123
2
3
4
5
6
7
8
await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到。
async function f() {
await Promise.reject('出错了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e));
// 出错了
2
3
4
5
6
7
8
只要一个 await 语句后面的 Promise 变为 reject,那么整个 async 函数都会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
2
3
4
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个 await 放在 try...catch 结构里面,这样不管这个异步操作是否成功,第二个 await 都会执行。
async function f() {
try {
await Promise.reject('出错了');
} catch (e) {}
return await Promise.resolve('hello world');
}
f().then(v => console.log(v));
// hello world
2
3
4
5
6
7
8
9
另一种方法是 await 后面的 Promise 对象再跟一个 catch 方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了').catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f().then(v => console.log(v));
// 出错了
// hello world
2
3
4
5
6
7
8
错误处理
如果 await 后面的异步操作出错,那么等同于 async 函数返回的 Promise 对象被 reject。
防止出错的方法,也是将其放在 try...catch 代码块之中。
async function f() {
try {
await new Promise(function(resolve, reject) {
throw new Error('出错了');
});
} catch (e) {}
return await 'hello world';
}
2
3
4
5
6
7
8
如果有多个 await 命令,可以统一放在 try...catch 结构中。
async function main() {
try {
const val1 = await firstStep();
const val2 = await secondStep(val1);
const val3 = await thirdStep(val1, val2);
console.log('Final: ', val3);
} catch (err) {
console.error(err);
}
}
2
3
4
5
6
7
8
9
10
11
使用注意点
- 第一点,前面已经说过,await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。
- 第二点,多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
2
3
4
5
6
7
8
- 第三点,await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
function dbFuc(db) {
//这里不需要 async
let docs = [{}, {}, {}];
// 可能得到错误结果
docs.forEach(async function(doc) {
await db.post(doc);
});
}
2
3
4
5
6
7
8
9
如果确实希望多个请求并发执行,可以使用 Promise.all 方法。当三个请求都会 resolved 时,下面两种写法效果相同。
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map(doc => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的写法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map(doc => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
目前,esm 模块加载器支持顶层 await,即 await 命令可以不放在 async 函数里面,直接使用。
// async 函数的写法
const start = async () => {
const res = await fetch('google.com');
return res.text();
};
start().then(console.log);
// 顶层 await 的写法
const res = await fetch('google.com');
console.log(await res.text());
2
3
4
5
6
7
8
9
10
11
async 函数的实现原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function*() {
// ...
});
}
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch (e) {
return reject(e);
}
if (next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(
function(v) {
step(function() {
return gen.next(v);
});
},
function(e) {
step(function() {
return gen.throw(e);
});
}
);
}
step(function() {
return gen.next(undefined);
});
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
实例:按顺序完成异步操作
实际开发中,经常遇到一组异步操作,需要按照顺序完成。比如,依次远程读取一组 URL,然后按照读取的顺序输出结果。
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
2
3
4
5
6
上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求。
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
2
3
4
5
6
7
8
9
10
11
12
就是改为 一个 async 只有一个 await,然后同时发几个 async.
异步遍历器
Iterator 接口这里隐含着一个规定,next 方法必须是同步的,只要调用就必须立刻返回值。也就是说,一旦执行 next 方法,就必须同步地得到 value 和 done 这两个属性。如果遍历指针正好指向同步操作,当然没有问题,但对于异步操作,就不太合适了。目前的解决方法是,Generator 函数里面的异步操作,返回一个 Thunk 函数或者 Promise 对象,即 value 属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而 done 属性则还是同步产生的。
ES2018 引入了”异步遍历器“(Async Iterator),为异步操作提供原生的遍历器接口,即 value 和 done 这两个属性都是异步产生。
异步遍历的接口
异步遍历器的最大的语法特点,就是调用遍历器的 next 方法,返回的是一个 Promise 对象。
asyncIterator
.next()
.then(
({ value, done }) => /* ... */
);
2
3
4
5
上面代码中,asyncIterator 是一个异步遍历器,调用 next 方法以后,返回一个 Promise 对象。因此,可以使用 then 方法指定,这个 Promise 对象的状态变为 resolve 以后的回调函数。回调函数的参数,则是一个具有 value 和 done 两个属性的对象,这个跟同步遍历器是一样的。
对象的异步遍历器接口,部署在 Symbol.asyncIterator 属性上面。不管是什么样的对象,只要它的 Symbol.asyncIterator 属性有值,就表示应该对它进行异步遍历。
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
asyncIterator
.next()
.then(iterResult1 => {
console.log(iterResult1); // { value: 'a', done: false }
return asyncIterator.next();
})
.then(iterResult2 => {
console.log(iterResult2); // { value: 'b', done: false }
return asyncIterator.next();
})
.then(iterResult3 => {
console.log(iterResult3); // { value: undefined, done: true }
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
由于异步遍历器的 next 方法,返回的是一个 Promise 对象。因此,可以把它放在 await 命令后面。
async function f() {
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
console.log(await asyncIterator.next());
// { value: 'a', done: false }
console.log(await asyncIterator.next());
// { value: 'b', done: false }
console.log(await asyncIterator.next());
// { value: undefined, done: true }
}
2
3
4
5
6
7
8
9
10
上面代码中,next 方法用 await 处理以后,就不必使用 then 方法了。
注意,异步遍历器的 next 方法是可以连续调用的,不必等到上一步产生的 Promise 对象 resolve 以后再调用。这种情况下,next 方法会累积起来,自动按照每一步的顺序运行下去。下面是一个例子,把所有的 next 方法放在 Promise.all 方法里面。
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
const [{ value: v1 }, { value: v2 }] = await Promise.all([
asyncIterator.next(),
asyncIterator.next()
]);
console.log(v1, v2); // a b
2
3
4
5
6
7
8
for await...of
新引入的 for await...of 循环,则是用于遍历异步的 Iterator 接口。
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// a
// b
2
3
4
5
6
7
上面代码中,createAsyncIterable()返回一个拥有异步遍历器接口的对象,for...of 循环自动调用这个对象的异步遍历器的 next 方法,会得到一个 Promise 对象。await 用来处理这个 Promise 对象,一旦 resolve,就把得到的值(x)传入 for...of 的循环体。
如果 next 方法返回的 Promise 对象被 reject,for await...of 就会报错,要用 try...catch 捕捉。
async function () {
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
}
}
2
3
4
5
6
7
8
9
for await...of 循环也可以用于同步遍历器。
(async function() {
for await (const x of ['a', 'b']) {
console.log(x);
}
})();
// a
// b
2
3
4
5
6
7
Node v10 支持异步遍历器,Stream 就部署了这个接口。下面是读取文件的传统写法与异步遍历器写法的差异。
// 传统写法
function main(inputFilePath) {
const readStream = fs.createReadStream(inputFilePath, {
encoding: 'utf8',
highWaterMark: 1024
});
readStream.on('data', chunk => {
console.log('>>> ' + chunk);
});
readStream.on('end', () => {
console.log('### DONE ###');
});
}
// 异步遍历器写法
async function main(inputFilePath) {
const readStream = fs.createReadStream(inputFilePath, {
encoding: 'utf8',
highWaterMark: 1024
});
for await (const chunk of readStream) {
console.log('>>> ' + chunk);
}
console.log('### DONE ###');
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
异步 Generator 函数
就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象。 在语法上,异步 Generator 函数就是 async 函数与 Generator 函数的结合。
async function* gen() {
yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
2
3
4
5
6
async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine();
}
} finally {
await file.close();
}
}
2
3
4
5
6
7
8
9
10
11
上面代码定义的异步 Generator 函数的用法如下。
(async function() {
for await (const line of readLines(filePath)) {
console.log(line);
}
})();
2
3
4
5
异步 Generator 函数可以与 for await...of 循环结合起来使用。
async function* prefixLines(asyncIterable) {
for await (const line of asyncIterable) {
yield '> ' + line;
}
}
2
3
4
5
如果异步 Generator 函数抛出错误,会导致 Promise 对象的状态变为 reject,然后抛出的错误被 catch 方法捕获。
async function* asyncGenerator() {
throw new Error('Problem!');
}
asyncGenerator()
.next()
.catch(err => console.log(err)); // Error: Problem!
2
3
4
5
6
7
一个异步 Generator 函数的执行器。
async function takeAsync(asyncIterable, count = Infinity) {
const result = [];
const iterator = asyncIterable[Symbol.asyncIterator]();
while (result.length < count) {
const { value, done } = await iterator.next();
if (done) break;
result.push(value);
}
return result;
}
2
3
4
5
6
7
8
9
10
async function f() {
async function* gen() {
yield 'a';
yield 'b';
yield 'c';
}
return await takeAsync(gen());
}
f().then(function(result) {
console.log(result); // ['a', 'b', 'c']
});
2
3
4
5
6
7
8
9
10
11
12
13
基本上,如果是一系列按照顺序执行的异步操作(比如读取文件,然后写入新内容,再存入硬盘),可以使用 async 函数;如果是一系列产生相同数据结构的异步操作(比如一行一行读取文件),可以使用异步 Generator 函数。
异步 Generator 函数也可以通过 next 方法的参数,接收外部传入的数据。
const writer = openFile('someFile.txt');
writer.next('hello'); // 立即执行
writer.next('world'); // 立即执行
await writer.return(); // 等待写入结束
2
3
4
最后,同步的数据结构,也可以使用异步 Generator 函数。
async function* createAsyncIterable(syncIterable) {
for (const elem of syncIterable) {
yield elem;
}
}
2
3
4
5
yield*语句也可以跟一个异步遍历器。
async function* gen1() {
yield 'a';
yield 'b';
return 2;
}
async function* gen2() {
// result 最终会等于 2
const result = yield* gen1();
}
2
3
4
5
6
7
8
9
10
与同步 Generator 函数一样,for await...of 循环会展开 yield*。
(async function() {
for await (const x of gen2()) {
console.log(x);
}
})();
// a
// b
2
3
4
5
6
7