含义
-ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么? 一句话,它就是 Generator 函数的语法糖。
一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。
async函数对 Generator 函数的改进,体现在以下四点。
(1)内置执行器。
Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。
(2)更好的语义。
async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。
co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
(4)返回值是 Promise。
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。
进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
基本用法
栗子君来也:
function timeOut(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
})
}
async function asyncPrint(value, ms) {
await timeOut(ms);
console.log(value);
}
asyncPrint('hello world', 5000);
这个例子就是5秒之后在控制台出现hello world。 那如果去掉async函数呢?
function timeOut(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
})
}
function asyncPrint(value, ms) {
timeOut(ms);
console.log(value);
}
asyncPrint('hello world', 5000);
当然是没有延迟5秒就会直接在控制塔输出hello world咯!
由于async函数返回的是 Promise 对象,可以作为await命令的参数。所以,上面的例子也可以写成下面的形式。
async function timeOut(ms) {
return await new Promise(resolve => {
setTimeout(resolve, ms);
})
}
async function asyncPrint(value, ms) {
await timeOut(ms);
console.log(value);
}
asyncPrint('hello hui', 5000);
语法
async函数的语法规则总体上比较简单,难点是错误处理机制。
返回 Promise 对象
async函数返回一个 Promise 对象。
async函数内部return语句返回的值,会成为then方法回调函数的参数。
async function f() {
return 'hello world';
}
f().then(v => console.log(v));
上面代码中,函数f内部return命令返回的值,会被then方法回调函数接收到。
async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。
async function f1() {
throw new Error('error');
}
f1().then(
v => console.log(v),
e => console.log(e)
)
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);
await命令
正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
async function f() {
return await 1234;
}
f().then(v => console.log(v));
上面的例子直接返回1234。
另一种情况是,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 actualTime = await new Sleep(1000);
console.log(actualTime);
})();
上面代码中,await命令后面是一个Sleep对象的实例。这个实例不是 Promise 对象,但是因为定义了then方法,await会将其视为Promise处理。
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('出错了111');
await Promise.resolve('hello world');
}
f();
上例中的第二个语句不会执行,因为因为第一个await语句状态变成了reject。
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。
async function f() {
try {
await Promise.reject('出错了123');
} catch (e) {
return await Promise.resolve('hello world')
}
}
f().then(v => console.log(v));
另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了2333').catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f().then(v => console.log(v));
使用注意点
1. 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
下面这段代码就是按照顺序执行的(即继发关系):
function getFoo() {
return new Promise(() => {
setTimeout(() => {
console.log('getFoo');
console.log(new Date());
}, 2000);
});
}
function getBar() {
return new Promise(() => {
setTimeout(() => {
console.log('getBar');
console.log(new Date());
}, 2000);
});
}
async function f() {
// 按顺序运行
var foo = await getFoo();
var bar = await getBar();
}
f();
函数运行后2s,输出'getFoo',再2s后输出'getBar'。
以下两种写法,getFoo和getBar都是同时触发,这样就会缩短程序的执行时间。
function getFoo() {
return new Promise(() => {
setTimeout(() => {
console.log('getFoo');
console.log(new Date());
}, 2000);
});
}
function getBar() {
return new Promise(() => {
setTimeout(() => {
console.log('getBar');
console.log(new Date());
}, 2000);
});
}
async function f1() {
// 同时触发写法一
// let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 同时触发写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
}
f();
以上两种写法,2s后,'getFoo'和'getBar'同时输出。
2. await命令只能用在async函数之中,如果用在普通函数,就会报错。
但是如果将forEach方法的参数改成async函数,也有问题。
function f() {
return new Promise(resolve => {
setTimeout(() => {
resolve(console.log(new Date().getSeconds()))
}, 1000);
});
}
function f1() {
let docs = [{}, {}, {}];
docs.forEach(async function(doc) {
await f(doc);
});
}
f1(); // 并发执行
// 33 33 33(33s时运行,得到三个33)
上面代码结果为三个相同的值,原因是这时三个f(doc)操作是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for循环。
function f() {
return new Promise(resolve => {
setTimeout(() => {
resolve(console.log(new Date().getSeconds()))
}, 1000);
});
}
async function f2() {
let docs = [{}, {}, {}];
for (let doc of docs) {
await f(doc);
}
}
f2(); // 继发执行
// 33 34 35 (33s时运行,得到三个不同的值)
最后,留一个小测试:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise(resolve => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
答案是什么呢?正确答案在下方哟!!
// script start
// async1 start
// async2
// promise1
// script end
// promise2
// async1 end
// setTimeout
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!