16-4 async和await (次重点)
上一节咱们学习了如何利用生成器实现异步操作。在生成器中,利用yield将异步操作挂起,外部 通过执行器让生成器的代码继续执行。这样,在生成器中,可以将异步的操作做成同步的效果, 实现了异步代码的简化。不过,这种方式需要编写外部的执行器,而执行器的代码写起来一点也 不简单。当然也可以使用一些插件,比如co模块来简化执行器的编写。
在ES7中,加入了 async函数来处理异步。它实际上只是生成器的一种语法糖而已,简化了外部 执行器的代码,同时利用await替代yield, async替代生成器的*号。下面还是来看个例子:
async function delay(){
await new Promise((resolve) => {setTimeout(()=>{resolve()},2000)}); console.log("go on);
} delay();
这个例子我们之前用生成器也写过,其中把生成器的(*)号被换成了async。async关键字必须写在 function的前面。如果是箭头函数,则写在参数的前面:
const delay = async () => {}
在函数中,第一句用了 await。它替代了之前的yield。后面同样需要跟上一个Promise对象。接下 来的打印语句会在上面的异步操作完成后执行。外部调用时就和正常的函数调用一样,但它的实 现原理和生成器是类似的。因为有了 async关键字,所以它的外部一定会有相应的执行器来执行 它,并在异步操作完成后执行回调函数。只不过这一切都被隐藏起来了,由JS引擎帮助我们完 成。我们需要做的就是加上关键字,在函数中使用await来执行异步操作。这样,可以大大的简 化异步操作。同时,能够像同步方法一样去处理它们。
接下来我们再来看看更细节的一些问题。await后面必须是一个Promise对象,这个很好理解。因 为该Promise对象会返回给外部的执行器,并在异步动作完成后执行resolve,这样外部就可以通 过回调函数处理它,并将结果传递给生成器。
那如果await后面跟的不是Promise对象又会发生什么呢?
const delay = async () => {
let data = await "hello";
console.log(data);
}
这样的代码是允许的,不过await会自动将hello字符串包装一个Promise对象。就像这样:
let data = await new Promise((resolve,reject) => resolve("hello"));
创建了 Promise对象后,立即执行resolve,并将字符串hello传递给外部的执行器。外部执行器的 回调函数再将这个hello传递回来,并赋值给data变量。所以,执行该代码后,马上就会输出字符 串hello。虽然代码能够这样写,但是await在这里的意义并不大,所以await还是应该用来处理异 步方法,同时该异步方法应该使用Promise对象。
async函数里面除了有await关键字外,感觉和其他函数没什么区别,那它能有返回值吗?答案是 肯定的,
const delay = async () => {
await new Promise((resolve) => {setTimeout(()=>{resolve()},2000)}); return "finish";
}
let result = delay();
console.log(result);
在delay函数中先执行等待2秒的异步操作,然后返回字符串finish。外部调用时我用一个变量接收 它的返回值。最后输出的结果是:
//没有任何等待立即输出
Promise { <pending> }
// 2秒后程序结束
我们可以看到,没有任何等待立即输出了一个Promise对象。而整个程序是在2秒钟后才结束的。 由此看出,获取async函数的返回结果实际上是return出来的一个Promise对象。假如return后面 跟着的本来就是一个Promise对象,那么它会直接返回。但如果不是,则会像await—样包裹一个 Promise对象返回。所以,想要得到返回的具体内容应该这样:
const delay = async () => {
await new Promise((resolve) => {setTimeout(()=>{resolve()},2000)}); return "finish";
}
let result = delay();
console.log(result);
result.then(function(data){
console.log("data:",data);
});
执行的结果:
//没有任何等待立即输出
Promise { <pending> }
//等待2秒后输出
data: finish
那如果函数没有任何返回值,得到的又是什么呢?我将上面代码中取掉return,再次运行:
//没有任何等待立即输出
Promise { <pending> }
//等待2秒后输出
data: undefined
可以看到,仍然可以得到Promise对象,但由于函数没有返回值,所以就不会有任何数据传递出 来,那么打印的结果就是undefined o
最后我们还是来梳理一下async的执行顺序。大致的顺序为:先执行同步代码,然后通过执行器 来执行async里面的每一句代码,如果有返回值,在外部要通过then()方法的回调函数来接
收,最后才被执行,示例如下:
const delay = async () => {
console.log('first');
let data = await new Promise((resolve,reject) => resolve("hello"));
console.log('aa');
console.log(data);
let data2 = await new Promise((resolve) => {setTimeout(()=>{resolve('Yes ')},2000)});
console.log(data2);
return 'World';
}
let result = delay();
console.log(result);
result.then(function(data){
console.log("data:",data);
});
console.log(11);
console.log(22);
// first
// Promise { <pending> }
// 11
// 22
// aa
// hello
// Yes
// data: World
效果:首先执行async函数,打印出first,然后是暂停里面的代码,返回一个promise,来到外 部。在外部执行完所有同步的代码,输出Promise { vpending> } , 11和22。接下来回到
async函数,输出aa和hello,然后等两秒钟后,输出Yes,最后回到外部,执行then()方 法,打印出data: World。
async的基本原理我们清楚了,下面我们把之前的AJAX例子用async重写下:
Mock.mock(/.json/,{
'stuents|5-10' : [{
'id|+1' : 1,
'name' : '@cname',
'gende r' : /[男女]/, //在正则表达式匹配的范围内随机 'age|15-30' : 1, //年龄在 15-30之间生成,值 1只是用来确定数据类型 'phone' : /1d{10}/,
'addr' : '@county(true)', //随机生成中国的一个省、市、县数据
'date' : "@date('yyyy-MM-dd')"
}]
});
async function getUsers(){ let data = await new Promise((resolve,reject) => {
$.ajax({ type:"get", url:"/users.json", success:function(data){ resolve(data)
}
});
}); console.log("data",data);
}
getUsers();
这是用JQuery的AJAX方法实现。
async function getUsers(){
let response = await fetch("/users");
let data = await response.json(); console.log("data",data);
} getUsers();
这是f etch方法的实现。
从这两个例子可以看出,async和生成器两种方式都很类似,但async可以不借助任何的第三方模 块,也更易于理解,async表示该函数要做异步处理。await表示后面的代码是一个异步操作,等 待该异步操作完成后再执行后面的动作。如果异步操作有返回的数据,则在左边用一个变量来接 收它。
我们知道,await可以让异步操作变为同步的效果。但是,有的时候为了提高效率,我们需要让 多个异步操作同时进行怎么办呢?方法就是执行异步方法时不加await,这样它们就可以同时进 彳丁,然后在获取结果时用await。比如:
function time(ms){
return new Promise((resolve,reject) => { setTimeout(()=>{resolve()},ms);
});
}
const delay = async () => {
let t1 = time(2000);
let t2 = time(2000);
await t1;
console.log("t1 finish"); await t2;
console.log("t2 finish");
}
delay();
我先把时间函数的异步操作封装成了函数,并返回Promise对象。在dela y函数中调用了两次time 方法,但没有用await。也就是说这两个时间函数的执行是"同时"(其实还是有先后顺序)进行 的。然后将它们的Promise对象分别用t1和t2表示。先用await t1。表示等待t1的异步处理完成, 然后输出t1 finish。接着再用await t2,等待t2的异步处理完成,最后输出t2 finish。由于这两个 时间函数是同时执行,而且它们的等待时间也是一样的。所以,当2秒过后,它们都会执行相应 的回调函数。运行的结果就是:等待2秒后,先输出t1 finish,紧接着立即输出t2 finish。
const delay = async () => {
await time(2000);
console.log("t1 finish");
await time(2000);;
console.log("t2 finish");
}
如果是这样写,那么执行的结果会是等待2秒后输出t1 finish。再等待2秒后输出t2 finish。async 确实是一个既好用、又简单的异步处理方法。但是它的问题就是不兼容老的浏览器,只有支持了 ES7的浏览器才能使用它。
最后,还需要注意一个问题:await关键字必须写在async定义的函数中。