JavaScript - Async
含义
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
有一个 Generator 函数,依次读取两个文件。
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
上面代码的函数 gen
可以写成 async
函数,就是下面这样。
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
一比较就会发现,async
函数就是将 Generator 函数的星号(*
)替换成 async
,将 yield
替换成 await
,仅此而已。
async
函数对 Generator 函数的改进,体现在以下四点:
内置执行器
Generator 函数的执行必须靠执行器,所以才有了
co
模块,而async
函数自带执行器。也就是说,async
函数的执行,与普通函数一模一样,只要一行。javascriptasyncReadFile();
上面的代码调用了
asyncReadFile
函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next
方法,或者用co
模块,才能真正执行,得到最后结果。更好的语义
async
和await
,比起星号和yield
,语义更清楚了。async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。更广的适用性
co
模块约定,yield
命令后面只能是 Thunk 函数或 Promise 对象,而async
函数的await
命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。返回值是 Promise
async
函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then
方法指定下一步的操作。进一步说,
async
函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await
命令就是内部then
命令的语法糖。
基本语法
返回 Promise 对象
async
函数返回一个 Promise 对象。
async
函数内部 return
语句返回的值,会成为 then
方法回调函数的参数。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
上面代码中,函数 f
内部 return
命令返回的值,会被 then
方法回调函数接收到。
async
函数内部抛出错误,会导致返回的 Promise 对象变为 reject
状态。抛出的错误对象会被 catch
方法回调函数接收到。
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log('resolve', v),
e => console.log('reject', e)
)
//reject Error: 出错了
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"
上面代码中,函数 getTitle
内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行 then
方法里面的 console.log
。
await 命令
正常情况下,await
命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
另一种情况是,await
命令后面是一个 thenable
对象(即定义了 then
方法的对象),那么 await
会将其等同于 Promise 对象。
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(
() => resolve(Date.now() - startTime),
this.timeout
);
}
}
(async () => {
const sleepTime = await new Sleep(1000);
console.log(sleepTime);
})();
// 1000
await
命令后面的 Promise 对象如果变为 reject
状态,则 reject
的参数会被 catch
方法的回调函数接收到。
async function f() {
await Promise.reject('出错了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了
注意,上面代码中,
await
语句前面没有return
,但是reject
方法的参数依然传入了catch
方法的回调函数。这里如果在await
前面加上return
,效果是一样的。
任何一个 await
语句后面的 Promise 对象变为 reject
状态,那么整个 async
函数都会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
上面代码中,第二个 await
语句是不会执行的,因为第一个 await
语句状态变成了 reject
。
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个 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
另一种方法是 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
错误处理
如果 await
后面的异步操作出错,那么等同于 async
函数返回的 Promise 对象被 reject
。
async function f() {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出错了
使用注意点
第一点,前面已经说过,await
命令后面的 Promise
对象,运行结果可能是 rejected
,所以最好把 await
命令放在 try...catch
代码块中。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
});
}
第二点,多个 await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
上面代码中,getFoo
和 getBar
是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有 getFoo
完成以后,才会执行 getBar
,完全可以让它们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
上面两种写法,getFoo
和 getBar
都是同时触发,这样就会缩短程序的执行时间。
第三点,await
命令只能用在 async
函数之中,如果用在普通函数,就会报错。
第四点,async 函数可以保留运行堆栈。
const a = () => {
b().then(() => c());
};
上面代码中,函数 a
内部运行了一个异步任务 b()
。当 b()
运行的时候,函数 a()
不会中断,而是继续执行。等到 b()
运行结束,可能 a()
早就运行结束了,b()
所在的上下文环境已经消失了。如果 b()
或 c()
报错,错误堆栈将不包括 a()
。
现在将这个例子改成 async
函数。
const a = async () => {
await b();
c();
};
上面代码中,b()
运行的时候,a()
是暂停执行,上下文环境都保存着。一旦 b()
或 c()
报错,错误堆栈将包括 a()
。