async/await 其实就是Generator的语法糖。 如果对Generator还不太熟悉可以先看这篇 听说你还没听过 ES6 的 Generator 函数?

  • async函数其实就相当于funciton *的作用
  • await就相当与yield的作用。

而在async/await机制中,自动包含了上述封装出来的spawn自动执行函数

async函数是ES6的新语法;使得异步操作变得更加方便。 使用关键字async来修饰,表示函数里面可能有异步操作,在函数内部使用await来表示异步。

  • async函数中如果没有await,那么和普通函数一样的
  • 一旦加了await ;那么await下面的代码就是异步的

微任务和宏任务

微任务和宏任务: 这两个都是异步队列中的任务。

  • 异步任务: setTimeoutsetInterval、事件、promisethenajaxasync await
  • 微任务: promisethen async ****await process.nextTick
  • 宏任务 : setTimeout setInterval ajax

执行顺序: 先执行同步任务,再执行异步任务;先执行微任务,再执行宏任务;

async函数

基本使用

async函数是 Generator 函数的语法糖。async声明该函数是异步的,且该函数会返回一个promise。

语法规则:

  • asyncfunction 的一个前缀,只有 async 函数中才能使用 await 语法
  • async 函数是一个 Promise 对象,有无 resolve 取决于有无在函数中 return
  • await 后面跟的是一个 Promise 对象,如果不是,则会包裹一层 Promise.resolve()

  • 相较于 Generator,async 函数的改进在于下面四点:

    • 内置执行器。Generator 函数的执行必须依靠执行器,而 aysnc 函数自带执行器,调用方式跟普通函数的调用一样;
    • 更好的语义async(异步) 和 await(等待) 相较于 *yield 更加语义化;
    • 更广的适用性。co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise对象。而 async 函数的 await 命令后面则可以是 Promise 或者 原始类型的值(**Numberstringboolean,此时等同于同步操作**);
    • 返回值是 Promiseasync 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then() 方法进行调用
1
2
3
4
5
6
7
8
9
10
11
12
// async函数自带执行器。async函数的执行,与普通函数执行一模一样;
function fn1() {
console.log(200);
}
async function fn() {
await fn1(); //同步
console.log(100); //异步
}
console.log(fn()); //输出promise实例,且是pending状态;
fn().then(function(a){
console.log(a); //输出undefined
})

async 返回一个promise的实例; 默认是成功态async函数内部 return语句返回的值 ,会成为 then方法回调函数的参数

1
2
3
4
5
6
7
8
async function fn() {
return 1;
}
console.log(fn());// async 返回一个promise的实例;默认是成功态;
//fn()的结果不受async function fn() {}函数中return的影响;fn()的结果永远都是promise实例;不然不能调用.then方法;
fn().then(function (a) { //由于fn()返回的是一个promise实例,所以可以调用.then方法;
console.log(a); //1 //.then中的结果会受fn()中return结果的影响;
});

下面这个例子中:fn1函数返回的是一个promise,fn函数中,只有当await后面的执行成功,也就是promise的状态变成Fulfilled ( resolve(参数) )了之后才会把await后面的代码放入微任务队列里面 ,整个async函数结束,继续执行后面的代码。 而且await的返回值就是resolve(参数) 中的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function fn1() {
// fn1函数中如果返回一个promise的实例,await下面的代码就是该promise的实例then中绑定函数中的代码;
return new Promise(function (resolve,reject) {
setTimeout(function () {
console.log(300);
resolve()
},200)
})
}
async function fn() {
await fn1();
// 如果fn1返回一个promise的实例,那么这个await下面的代码当上面调用resolve才会执行;
console.log(200); //这句代码受fn1函数中return的promise实例的结果的影响;
}
fn().then(function () {
}).then(function () {
})

// 输出:
// 300
// 200

async 函数返回的 Promise 对象,必须等到内部所有的 await 命令的 Promise 对象执行完,才会发生状态改变 也就是说,只有当 async 函数内部的异步操作都执行完,才会执行 then 方法的回调

async函数的多种形式

  1. 函数声明async function fn(){}
  2. 函数表达式let fn = async function(){}
  3. 箭头函数let fn = async () => {}
  4. 对象的方法let obj = { async fn(){} }; obj.fn().then(...)
  5. class的方法
1
2
3
4
5
6
7
8
9
10
11
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(…);

async函数的错误处理

  • 如果 async 函数内部抛出异常,则会导致返回的 Promise 对象状态变为 reject 状态。抛出的错误而会被 catch 方法回调函数接收到。
  • 如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject。 防止出错的方法,也是将其放在try...catch代码块之中

await的多种类型

await+Promise

这是最常见的场景。

await 会等待Promise的状态改为fullfilled

  • 如果成功,那么会 async函数剩余任务(也就是await后面的代码)推入到微任务队列
  • 如果失败,那么剩余任务不会被推入微任务队列执行,它会 返回Promise.reject(err)
1
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
44
async function async1() {                
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 start')
return new Promise((resolve, reject) => {//p2 p2.then()方法执行完才代表async2函数执行完毕
resolve()
console.log('async2 promise')
// 把p2.then()放入微队列
// 此时async2没执行结束=>async1没执行结束,继续执行下面的代码
})
}
console.log('script start')
setTimeout(function () { // 将其放入宏任务队列,等微任务队列执行完了再执行
console.log('setTimeout')
}, 0)
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve() // 把下面两个then()方法放到微任务队列
})
.then(function () {
console.log('promise2')
})
.then(function () {
console.log('promise3')
})
// 此时微任务队列:‘p2.then(),上面两个then()方法,也就是console.log('promise2')和console.log('promise3')’
console.log('script end')
// 至此开始执行微任务队列:async2函数执行完毕=>把async1中‘console.log('async1 end')’放入微队列
// 继续依次执行微队列,执行完后执行宏队列
// 输出:
// script start
// async1 start
// async2 start
// async2 promise
// promise1
// script end
// promise2
// promise3
// async1 end
// setTimeout

await+普通值

即使await右边非函数,只是一个普通的数值,但它本质上是将其转化为 Promise.resolve(普通值) ,所以会返回一个成功的promise

因此 当await等待到了成功的结果后,它会将async函数剩余内容(也就是await后面的代码)推入到微任务队列中等待执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 async function run() {
console.log('start 1')
const res = await 2
// 会将2包装成Promise.resolve(2),并将其中的参数2作为await的返回值传给res
// 此时run()函数里面的代码已经全部“遍历”完,继续执行run()后面的代码=>输出3
console.log(res)
console.log('end')
}
run()
console.log('3')
// 输出:
// start 1
// 3
// 2
// end

await+函数

如果await 右边是一个函数,它会立刻执行这个函数,而且只有当这个函数执行结束后(即函数完成)!才会将async剩余任务(也就是await后面的代码)推入微任务队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function fn() {
console.log('fn start')
console.log('fn end')
}
async function run() {
console.log('start 1')
const res = await fn() // 函数fn()没有return,所以res是undefined
console.log(res)
console.log('end')
}
run()
console.log('3')
// 输出:
// start 1
// fn start
// fn end
// 3
// undefined
// end
1
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
async function async1() {
console.log(1)
await async2()
console.log(2)
}

const async2 = async () => {
await (async () => { // await后面是一个立即执行函数1
await (() => { // 立即执行函数1里面的await后面是一个立即执行函数2
console.log(3) // 打印3会直接执行,接着立即执行函数2返回undefined
})()
console.log(4)
// 立即执行函数2返回undefined后,把console.log(4)和return undefined放入微任务队列
// 此时async2没执行结束,async1也没执行结束,只能继续向下执行console.log(7)和async3
})()
}

const async3 = async () => {
Promise.resolve().then(() => {
console.log(6) // 把console.log(6)放入微任务队列
// 此时微任务队列:‘console.log(4),return undefined(立即执行函数1执行结束),console.log(6)’
// 之后依次执行微任务:打印4,return undefined(立即执行函数1执行结束)
// 此时会把async2函数的剩余内容也就是return undefined(async2函数执行结束)加入微队列
// 依次执行微队列中的console.log(6),return undefined(async2函数执行结束)
// 此时async2完成,把async1中的剩余内容,也就是await下面的console.log(2)放入微队列
// 执行微队列:输出2
})
}

async1()

console.log(7)

async3()
// 输出:
// 1
// 3
// 7
// 4
// 6
// 2

如果await后面的返回的promise状态变成rejected ,那么它 将不会再把剩余任务推入到微任务队列,跳过整个async函数继续执行后面的代码,并在执行完之后Uncaught

await+定时器(函数)

定时器setTimeOut()setInterval()也都是函数,不过与普通函数不同的是,定时器函数返回的是一个 定时器ID

1
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
44
async function async1() {
console.log(1)
await async2()
console.log(2)
}

const async2 = async () => {
await setTimeout((_) => {
Promise.resolve().then((_) => {
console.log(3)
})
console.log(4)
}, 0)
// 这里有个坑:没有返回就代表return undefined,之后被包装成Promise.resolve(undefined)
// 执行async2时,先把整个setTimeout放进宏任务队列,返回一个定时器ID,也就是await ID
// 也就是await Promise.resolve(ID)
// 接着就把“return undefined(也就是async2执行完毕)”放入微任务队列
// 此时继续执行console.log(7),async3
}

const async3 = async () => {
// 下面把console.log(6)放入微任务队列
// 此时的微任务队列:‘return undefined(也就是async2执行完毕),console.log(6)’
Promise.resolve().then(() => {
console.log(6)
})
// 此时会先执行微任务,再执行宏任务
// 故把微任务队列中的“return undefined(也就是async2执行完毕)”执行,return一个Promise.resolve(undefined)
// 那么在async1中await Promise.resolve(undefined),此时会把‘console.log(2)’放入微任务队列
// 此时的微任务队列:‘console.log(6),console.log(2)’,执行(此时微队列空了),依次输出6,2
// 再执行宏任务:把console.log(3)放入微任务队列,接着执行console.log(4)
// 再执行微队列,打印出3
}

async1()
console.log(7)
async3()
// 输出:
// 1
// 7
// 6
// 2
// 4
// 3

为什么要使用async/await?

都已经有Promise了,为什么还要使用async/await

可以隐藏 Promise ,更易于理解

假设我们想请求一个接口,然后把响应的数据打印出来,并且捕获异常。

1
2
3
4
5
6
7
8
9
function logFetch(url) {
return fetch(url)
.then(response => response.text())
.then(text => {
console.log(text);
}).catch(err => {
console.error('fetch failed', err);
});
}
1
2
3
4
5
6
7
8
9
async function logFetch(url) {
try {
const response = await fetch(url);
console.log(await response.text());
}
catch (err) {
console.log('fetch failed', err);
}
}

虽然代码的行数差不多,但是代码看起来更加简洁,少了很多 then 的嵌套。请求一个接口数据,然后打印,就像你看到的,很简单。

用同步的思路写异步逻辑

想获取一个网络资源的大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getResponseSize(url) {
return fetch(url).then(response => {
const reader = response.body.getReader();
let total = 0;

return reader.read().then(function processResult(result) {
if (result.done) return total;

const value = result.value;
total += value.length;
console.log('Received chunk', value);

return reader.read().then(processResult);
})
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const processResult = (result) =>{
if (result.done) return total;

const value = result.value;
total += value.length;
console.log('Received chunk', value);

return reader.read().then(processResult);
}

function getResponseSize(url) {
return fetch(url).then(response => {
const reader = response.body.getReader();
let total = 0;

return reader.read().then(processResult)
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function getResponseSize(url) {
const response = await fetch(url);
const reader = response.body.getReader();
let result = await reader.read();
let total = 0;

while (!result.done) {
const value = result.value;
total += value.length;
console.log('Received chunk', value);
// get the next result
result = await reader.read();
}

return total;
}

因为 await 表达式会阻塞运行,甚至可以直接阻塞循环,所以整体看起来像同步的代码,也更符合直觉,更容易读懂这个代码。

解决了Promise参数传递麻烦的弊端

假设一个业务,分多个步骤完成,每个步骤都是异步的而且依赖于上一个步骤的结果,并且每一个步骤都需要之前每个步骤的结果。。用 setTimeout 来模拟异步操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* 传入参数n,表示这个函数执行的时间(毫秒)
* 执行的结果是 n+200,这个值将用于下一步骤
*/
function takeLongTime(n){
return new Promise((resolve) => {
setTimeout(() => resolve(n + 200),n);
})
}
function step1(n){
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(m,n){
console.log(`step2 with ${m} + ${n}`);
return takeLongTime(m + n);
}
function step3(k,m,n){
console.log(`step3 with ${k} + ${m} + ${n}`);
return takeLongTime(k + m + n);
}
1
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
function doIt() {
console.time('doIt');
let time1 = 300;
step1(time1)
.then((time2) => {
return step2(time1,time2)
.then((time3) => [time1,time2,time3])//step3需要用到time1,time2,time3,因此需要返回
})
.then((times) => {
let [time1,time2,time3] = times;
return step3(time1,time2,time3)
})
.then((result) => {
console.log(`result is ${result}`);
console.timeEnd('doIt');
})
}

doIt();

//执行结果为:
//step1 with 300
//step2 with 300 + 500
//step3 with 300 + 500 + 1000
//result is 2000
//doIt: 2919.49609375ms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function doIt() {
console.time('doIt');
let time1 = 300;
let time2 = await step1(time1);//将Promise对象resolve(n+200)的值赋给time2
let time3 = await step2(time2,time1);
let result = await step3(time3,time2,time1);
console.log(`result is ${result}`);
console.timeEnd('doIt');
}

doIt();

//执行结果为:
//step1 with 300
//step2 with 500 + 300
//step3 with 1000 + 500 + 300
//result is 2000
//doIt: 2916.655029296875ms

一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,而async/await则解决了Promise参数传递麻烦的弊端

async/await的错误捕获

await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好 await 命令放在 try...catch 代码块中,这样就不会影响后面代码的运行

因为如果await后面的返回的promise状态变成rejected,那么它将不会再把剩余任务推入到微任务队列,剩余的代码也就不会再执行了。(await不能提取reject的结果)

1
2
3
4
5
6
7
8
9
const fn = async ()=> {
console.log('我在await Promise之前');
const result = await Promise.reject('我是错误信息');
console.log(result);
console.log('我在await Promise之后');
}

fn()
console.log('我在fn()之后')

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fn = async ()=> {
console.log('我在await Promise之前');
try{
const result = await Promise.reject('我是错误信息');
console.log(result);
}catch(e){
console.log('error:', e)
}finally {
console.log('我在finally里面,始终会执行');

}
console.log('我在await Promise之后,我没有受到影响');
}

fn()
console.log('我在fn()之后')

运行结果:

可以看到虽然reject了,但仍console.log('我在await Promise之后,我没有受到影响');,说明不影响后面的代码执行。

小心await阻塞

由于 await 能够阻塞 async 函数的运行,所以代码看起来更像同步的代码,更容易阅读和理解。但是要小心 await 阻塞,因为有些阻塞是不必要的,不恰当使用可能会影响代码的性能。

假如要把一个网络数据和本地数据合并,错误的实例可能是这样子:

1
2
3
4
5
async function combineData(url, file) {
let networkData = await fetch(url)
let fileData = await readeFile(file)
console.log(networkData + fileData)
}

其实我们不用等一个文件读完了,再去读下个文件,我们可以两个文件一起读,读完之后再进行合并,这样能提高代码的运行速度。我们可以这样写:

1
2
3
4
5
6
7
async function combineData(url, file) {
let fetchPromise = fetch(url)
let readFilePromise = readFile(file)
let networkData = await fetchPromise
let fileData = await readFilePromise
console.log(networkData + fileData)
}

这样的话,就可以同时 网络请求 和 读取文件 了,可以节省很多时间。这里主要是利用了 Promise 一旦创建就立刻执行的特点——fetchPromise readFilePromise 是两个异步操作的 Promise 对象,它们被创建后立即开始执行(一起执行的),而不是顺序执行

可以直接使用 Promise.all 的方式来处理,或者 await 后面跟 Promise.all

1
2
3
4
5
async function combineData(url, file) {
let promises = [fetch(url), readFile(file)]
let [networkData, fileData] = await Promise.all(promises)
console.log(networkData + fileData)
}

async/await的实现原理

下面的代码实现了async/await的部分功能,yield相当于await,但是我们发现代码中存在了多次的嵌套调用,这还取决于 yield 的数量,这明显是不能容忍的,与此同时,gen 最终返回的也不是一个 Promise 对象,因此我们可以通过一个高阶函数来解决问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function p(num) {
return Promise.resolve(num * 2)
}

function* generator() {
const value1 = yield p(1)
const value2 = yield p(value1)
return value2
}

const gen = generator();

const next1 = gen.next()
next1.value.then((res1) => {
console.log(res1)

const next2 = gen.next(res1)
next2.value.then((res2) => {
console.log(res2)
})
})
// 2 4

所谓高阶函数,就是在函数中返回函数。

因为async函数是一个可以返回Promise的函数,所以可以在高阶函数中返回一个返回值为 Promise 对象的函数:(同时处理一下嵌套调用问题=>改成递归调用

1
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
44
function p(num) {
return Promise.resolve(num * 2)
}

function* generator() {
const value1 = yield p(1)
const value2 = yield p(value1)
return value2
}

function higherOrderFn(generatorFn) {
return () => {
return new Promise((resolve, reject) => {
let gen = generatorFn()
// 链式处理yield
const doYield = (val)=>{
console.log(val)
let res

try{
res = gen.next(val)
}catch(err){
reject(err)
}

const {value,done} = res
// done === true 函数结束,resolve结果
if(done){
return resolve(value)
}else{
// 未结束,处理 value,同时传参
value.then((val)=>{doYield(val)})
}
}

doYield()
})
}
}

const asyncFn = higherOrderFn(generator)()
// undefined
// 2
// 4

至此,generator 的函数体已经能和 async 函数实现契合了。