Async指南

106 阅读5分钟
本文将参考ES6文档一步一步实现一个async 函数

首先、我们看一段关于async调用的代码

async function task() {
    let resultOne = await new Promise((resolve) => {
      setTimeout(resolve.bind(null, 1), 2000);
    })
    let resultTwo = await new Promise((resolve) => {
      setTimeout(resolve.bind(null, 2), 2000);
    })
    return resultOne + resultTwo;
  }
  const now = Date.now();
  task().then(res => {
    console.warn(`res = ${res}; duration = ${Date.now() - now}`);
  });

输出结果是res = 3, duration 约等于 4000,也就是说后面的task要等前面的task执行完毕才能开始运行。 而我们知道一个function一旦调用了、代码就会依次往下执行、如何实现等待的效果?

如果我们能将函数拆开,控制一段一段、甚至一行一行的执行就可以实现了,Generator 函数就是用来解决这个问题的。


Generator 函数简介

Generator 函数协程ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)

整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。Generator 函数的使用方式可以参考代码:

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
hw.next();
// { value: 'hello', done: false }

hw.next();
// { value: 'world', done: false }

hw.next();
// { value: 'ending', done: true }

hw.next();
// { value: undefined, done: true }

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。这两个特征就对应的asyncawait

调用 Generator 函数后,该函数并不执行、而且返回一个遍历器、然后必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。


开始实现

有了 Generator 函数函数后、我们就可以尝试实现一个async了。首先第一步就是去掉async关键字,封装一个内部Generator 函数,将async函数内部的代码全部复制过来扔进Generator 函数、但是我们需要把await改成成yield、最后返回一个Promise

就拿上面的task函数来进行举例:

function task() {
  function *innerTask() {
    // async函数方法体拷贝过来、把await改成 yield
    let resultOne = yield new Promise((resolve) => {
      setTimeout(resolve.bind(null, 1), 2000);
    })
    let resultTwo = yield new Promise((resolve) => {
      setTimeout(resolve.bind(null, 2), 2000);
    })
    return resultOne + resultTwo;
  }
  // 返回一个Promise
  return new Promise((resolve, reject) => {
  })
}

我们已经成功去掉了asyncawait,剩下就是利用Generator 函数next方法去按顺序执行任务就可以了。

function task() {
   function *innerTask() {
      // async函数方法体拷贝过来、把await改成 yield
      let resultOne = yield new Promise((resolve) => {
        setTimeout(resolve.bind(null, 1), 2000);
      })
      let resultTwo = yield new Promise((resolve) => {
        setTimeout(resolve.bind(null, 2), 2000);
      })
      return resultOne + resultTwo;
    }
   // 返回一个Promise
   return new Promise((resolve, reject) => {
      let g = innerTask();
      function run(next) {
        let result;
        try {
          result = next();
        } catch(e) {
          return reject(e);
        }
        if (!result.done) {
          run(() => g.next(result.value));
        } else {
          resolve(result.value);
        }
      }
      run(() => g.next(undefined));
    })
  }

  const now = Date.now();
  task().then(res => {
    console.warn(`res = ${res}; duration = ${Date.now() - now}`);
  });

本质上是一个递归依次调度任务执行,但是输出结果res = [object Promise][object Promise]; duration = 1,不符合预期。问题出在哪里了呢?

是因为我们没有等第一个任务结束了就开始了第二个任务、那么如何实现任务的挂起排队呢,很简单、只要把第二个任务注册到第一个任务的then方法里就可以了,所以我们只需要稍微调整下代码即可:

function task() {
   function *innerTask() {
      // async函数方法体拷贝过来、把await改成 yield
      let resultOne = yield new Promise((resolve) => {
        setTimeout(resolve.bind(null, 1), 2000);
      })
      let resultTwo = yield new Promise((resolve) => {
        setTimeout(resolve.bind(null, 2), 2000);
      })
      return resultOne + resultTwo;
    }
   // 返回一个Promise
   return new Promise((resolve, reject) => {
      let g = innerTask();
      function run(next) {
        let result;
        try {
          result = next();
        } catch(e) {
          return reject(e);
        }
        if (!result.done) {
          Promise.resolve(result.value).then((res) => {run(() => g.next(res))}, (e) => g.throw(e));
        } else {
          resolve(result.value);
        }
      }
      run(() => g.next(undefined));
    })
  }

  const now = Date.now();
  task().then(res => {
    console.warn(`res = ${res}; duration = ${Date.now() - now}`);
  });

这样一个自定义async函数就基本成型了。 上述代码调整的原理是利用了promise.reslove的特性,如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例, 如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved


最后一点小猜想

为什么用了awaittry-catch就能捕获promise内部的错误了?

本质原因是Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

function *gen() {
    try {
      yield 1;
      yield 2;
    } catch(e) {
      console.log('catch error = ' + e);
    }
 }
 var g = gen();
 g.next();
 // g.throw('mistake!');
 g.next();
 g.throw('mistake!');

Generator 函数执行一次next之后,直至状态变为done之前,我们在任何地方调用g.throw都会被内部的try-catch捕获。

结合这个特性,我们只需要在每个任务后面加上一个catch然后调用g.throw就可以触发外侧的try-catch了。这里补充一句:then(func1, func2) 本质上等价于then(func1).catch(func2),后者是前者的语法糖。

最后总结一下,asyncawait本质上就是Generator 函数yield的语法糖,以后看见asyncawait直接用Generator yield替换即可。