现代化的异步编程方式——async / await

171 阅读8分钟

揭开 async/await 的神秘面纱!了解微任务与宏任务、全面解析async函数的多种形式及错误处理、各种类型的 await 使用,以及它们在简化异步编程中的强大优势与实现原理。

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() 方法进行调用
// 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方法回调函数的参数

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(参数) 中的参数

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的方法
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)
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后面的代码)推入到微任务队列中等待执行

 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后面的代码)推入微任务队列

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
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

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 ,更易于理解

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

function logFetch(url) {
  return fetch(url)
    .then(response => response.text())
    .then(text => {
      console.log(text);
    }).catch(err => {
      console.error('fetch failed', err);
    });
}
async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  }
  catch (err) {
    console.log('fetch failed', err);
  }
}

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

用同步的思路写异步逻辑

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

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);
    })
  });
}
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)
  });
}
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 来模拟异步操作:

/*
 * 传入参数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);
}
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
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的结果)

const fn = async ()=> {
  console.log('我在await Promise之前');
  const result = await Promise.reject('我是错误信息');
  console.log(result);
  console.log('我在await Promise之后');
}

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

运行结果:

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 阻塞,因为有些阻塞是不必要的,不恰当使用可能会影响代码的性能。

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

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

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

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

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 对象,因此我们可以通过一个高阶函数来解决问题。

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 对象的函数:(同时处理一下嵌套调用问题=>改成递归调用

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 函数实现契合了。