现在以下几种async
函数的用法都是可行的。
Async函数声明:
async function foo(){}
Async函数表达式:
const foo = async function(){}
Async方法定义:
let obj = {async foo(){}}
Async箭头函数:
const foo = async () => {}
Async函数通常返回的是 Promise:
async function asyncFunc() {
return 123;
}
asyncFunc()
.then(x => console.log(x));
123
async function asyncFunc() {
throw new Error('Problem!');
}
asyncFunc()
.catch(err => console.log(err));
Error: Problem!
只有操作符await
可以放在 Async 函数里用来处理返回的 Promise 对象结果。所以await
的处理结果会随着 Promise 的状态不同而不同。
一个简单的异步处理:
async function asyncFunc() {
const result = await otherAsyncFunc();
console.log(result);
}
// 等价于:
function asyncFunc() {
return otherAsyncFunc()
.then(result => {
console.log(result);
});
}
顺序处理多个异步结果:
async function asyncFunc() {
const result1 = await otherAsyncFunc1();
console.log(result1);
const result2 = await otherAsyncFunc2();
console.log(result2);
}
// 等价于:
function asyncFunc() {
return otherAsyncFunc1()
.then(result1 => {
console.log(result1);
return otherAsyncFunc2();
})
.then(result2 => {
console.log(result2);
});
}
平行处理多个异步结果:
async function asyncFunc() {
const [result1, result2] = await Promise.all([
otherAsyncFunc1(),
otherAsyncFunc2(),
]);
console.log(result1, result2);
}
// 等价于:
function asyncFunc() {
return Promise.all([
otherAsyncFunc1(),
otherAsyncFunc2(),
])
.then([result1, result2] => {
console.log(result1, result2);
});
}
处理错误异常:
async function asyncFunc() {
try {
await otherAsyncFunc();
} catch (err) {
console.error(err);
}
}
// 等价于:
function asyncFunc() {
return otherAsyncFunc()
.catch(err => {
console.error(err);
});
}
在解释async
函数之前,我想通过把Promises
和generator
结合起来,用看起来像同步代码的方式去模拟一下异步。
对于处理获取一次性结果的异步函数,Promises
是目前最流行的方式。下面是一个使用fetch
方法获取文件的示例:
function fetchJson(url) {
return fetch(url)
.then(request => request.text())
.then(text => {
return JSON.parse(text);
})
.catch(error => {
console.log(`ERROR: ${error.stack}`);
});
}
fetchJson('http://example.com/some_file.json')
.then(obj => console.log(obj));
co
是一个基于Promises
和generator
的实现,也能够让你以书写同步代码的方式来实现上面的示例。
const fetchJson = co.wrap(function* (url) {
try {
let request = yield fetch(url);
let text = yield request.text();
return JSON.parse(text);
}
catch (error) {
console.log(`ERROR: ${error.stack}`);
}
});
co
在generator
回调函数中每次检测到一个带有yield
操作符的方法,就会产生一个Promise
对象,co
会先暂停回调代码的执行,直到Promise
对象的状态发生变化再继续执行。无论Promise
的状态为resolved
或者rejected
,yield
都会将相应的结果值返回。
详细说明一下async
函数的执行过程:
async
函数在开始执行的时候,通常都是返回一个Promise
对象- 函数体被执行之后,你可以使用
return
或者throw
直接完成执行过程。也可以使用await
暂时完成执行过程,然后根据情况再继续执行 - 最终返回一个
Promise
对象 then
和catch
中的callback只有在当前所有代码执行完毕之后,才会被执行。从下面的输出结果可以看出函数asyncFunc
的返回值等到所有代码包括循环逻辑都执行完毕之后,才最终得以被输出
async function asyncFunc() {
console.log('asyncFunc()'); // (A)
return 'abc';
}
asyncFunc().
then(x => console.log(`Resolved: ${x}`)); // (B)
console.log('main');
for(let i=0; i<5; i++){
console.log(i)
}
asyncFunc()
main
0
1
2
3
4
Resolved: abc
使用return
去Resolve
一个async
函数状态,是一种很标准的操作方式。这意味着你可以:
- 直接返回一个非
Promise
对象类型的值,作为Resolve
状态的参数值 - 返回的
Promise
对象代表了当前async
的函数状态
使用async
函数最常犯的一个错误就是忘记添加await
关键字,例如下面的示例中value
指向了一个Promise
对象,但是忘了添加await
关键字,所以输出的可能并不是你想要的结果:
async function asyncFunc() {
const value = otherAsyncFunc(); // missing `await`!
···
}
await
可以感知到后面跟的 Promise 异步函数是否有结果值,例如在下面的示例中,await
可以确保step1()
执行完之前,不会执行foo()
的剩余逻辑代码:
async function foo() {
await step1(); // (A)
···
}
有时候你仅仅是想触发一个异步函数计算,并不想知道它会何时完成。例如在下面示例中,我们并不关心写文件的操作何时完成,只要它们是按正确的顺序执行就可以了。最后一行的await
只是为了确保关闭写文件的操作能被成功执行即可。
async function asyncFunc() {
const writer = openFile('someFile.txt');
writer.write('hello'); // don’t wait
writer.write('world'); // don’t wait
await writer.close(); // wait for file to close
}
多个await
异步函数是顺序执行的关系,想要它们同时执行就得使用Promise.all()
了:
async function foo() {
const result1 = await asyncFunc1();
const result2 = await asyncFunc2();
}
async function foo() {
const [result1, result2] = await Promise.all([
asyncFunc1(),
asyncFunc2(),
]);
}
有一个需要知道的限制是,await
操作符只会影响async
函数的直接作用域环境。因此,你不能在一个async
方法的回调函数中直接使用await
。
下面的示例是根据 url 下载一些资源:
async function downloadContent(urls) {
return urls.map(url => {
// Wrong syntax!
const content = await httpGet(url);
return content;
});
}
像上面这种在普通的箭头函数中使用await
是无法运行的,会抛出语法错误。那我们应该怎么用,像下面这样吗?
async function downloadContent(urls) {
return urls.map(async (url) => {
const content = await httpGet(url);
return content;
});
}
function httpGet(url){
return new Promise(function(resolve, reject){
setTimeout(function(){
if(url.indexOf('a') > -1){
resolve('a.com');
}else{
resolve('b.com');
}
}, 2000)
});
}
const contents = downloadContent(['http://a.com', 'http://b.com']);
console.log(contents);
contents.then((url) => {
console.log(url);
});
输出结果:
你会发现代码中有两个问题:
- 返回值是一个包含两个
Promise
对象的数组,并不是我们期望的包含resolve
返回值的数组 await
只能暂停箭头回调函数里的httpGet()
,map
本身的回调函数执行完成之后,并不能影响外层的downloadContent ()
也执行完成
我们用Promise.all()
来修复这两个问题,把返回的 包含两个Promise
对象的数组 转换成 包含两个数组元素的Promise
对象,看如下示例:
async function downloadContent(urls) {
const promiseArray = urls.map(url => httpGet(url));
return await Promise.all(promiseArray);
}
function httpGet(url){
return new Promise(function(resolve, reject){
setTimeout(function(){
if(url.indexOf('a') > -1){
resolve('a.com');
}else{
resolve('b.com');
}
}, 2000)
});
}
const contents = downloadContent(['http://a.com', 'http://b.com']);
console.log(contents);
contents.then((url) => {
console.log(url);
});
输出结果:
OK,现在输出结果是正确的了。但是这段代码还是有一点点低效的地方需要改进,downloadContent ()
函数里首先用await
展开了Promise.all()
的返回结果,后面又用return
包装了一次,其实我们用return
直接返回Promise.all()
即可:
async function downloadContent(urls) {
const promiseArray = urls.map(url => httpGet(url));
return Promise.all(promiseArray);
}
我们这次换成forEach()
方法来模拟输出若干文件内容。很显然,下面的示例会抛出语法错误,因为你不能在普通的箭头函数中直接使用await
:
async function logContent(urls) {
urls.forEach(url => {
// Wrong syntax
const content = await httpGet(url);
console.log(content);
});
}
那我把代码修改成如下:
async function logContent(urls) {
urls.forEach(async url => {
const content = await httpGet(url);
console.log(content);
});
// Not finished here
}
这次代码倒是运行了,但是httpGet()
方法返回resolve
状态的操作是异步的,也就是说当forEach()
方法已经返回之后,它的callback
还并没有执行完成。修复此问题,只需要将代码做一下更改:
async function logContent(urls) {
for (const url of urls) {
const content = await httpGet(url);
console.log(content);
}
}
这段示例中的httpGet()
执行顺序是线性的,每一次的调用必须要等待上一次执行完毕。如果想要改成并行的执行顺序,就得用Promise.all()
了:
async function logContent(urls) {
await Promise.all(urls.map(
async url => {
const content = await httpGet(url);
console.log(content);
}));
}
map()
方法创建了一个Promise
对象数组。我们并不关心这几个Promise
对象的履行结果,只要它们履行了即可。也就是loginContent()
方法执行完成就可以了。在这个示例中除非把Promise.all()
直接返回,否则此函数的结果只会包含若干`undefined。
async
函数的基础就是Promise
,所以充分理解下面的示例非常重要。尤其是在那些没有使用Promise
机制的老代码中使用async
函数时,你可能除了直接使用Promise
之外没有别的选择。
下面是一个在XMLHttpRequest
中使用Promise
的示例:
function httpGet(url, responseType="") {
return new Promise(
function (resolve, reject) {
const request = new XMLHttpRequest();
request.onload = function () {
if (this.status === 200) {
// Success
resolve(this.response);
} else {
// Something went wrong (404 etc.)
reject(new Error(this.statusText));
}
};
request.onerror = function () {
reject(new Error(
'XMLHttpRequest Error: '+this.statusText));
};
request.open('GET', url);
xhr.responseType = responseType;
request.send();
});
}
XMLHttpRequest
的 API 设计都是基于 callback 的。使用async
函数就意味着你要在内层回调函数里使用return
和throw
返回Promise
对象的状态,但这肯定是不可能的。因此,在这情况中使用async
的风格就是:
- 使用
Promise
直接构建一个异步的基元 - 通过
async
函数来使用这些基元
在一个模块或者 script 的顶级作用域中使用await
,可以像下面这样:
async function main() {
console.log(await asyncFunction());
}
main();
或者
(async function () {
console.log(await asyncFunction());
})();
或者
(async () => {
console.log(await asyncFunction());
})();
不用太担心那些未处理rejections
,以前这种情况可能都是静默失败,不过现在大多数的现代浏览器都会抛出一个未处理的异常:
async function foo() {
throw new Error('Problem!');
}
foo();