揭秘 async/await 隐藏的原理

830 阅读4分钟

前言

说到异步编程方法,熟知的可能有回调函数Promiseasync/await三种解决方案。

回调函数:把任务的第二段单独写在一个函数里,等需要重新执行这个任务时,就直接调用这个函数。 Promise拆解实现 Promise 及其周边 。详细讲解点击这里
async/await:ES2017标准引入,是本文的重点,下面细扣一下。

首先,async函数是Generator函数的语法糖。Generator又是什么?

GeneratorES6提供的异步编程解决方案,语法行为与传统函数完全不同。传统函数一旦开始运行,就会运行到最后或遇到return时结束,运行期间不能打断,也不能外部传值进函数体内。而Generator函数打破固有思维,使打破函数完整运行成为可能。

文章内容是整合参考中的两本书,提取重要的信息点,方便路过的朋友快速掌握。

Generator 函数

协程

模式上,Generator函数类似协程,多个线程互相协作,完成异步任务。其运行流程大致如下。

  • 第一步,协程 A 开始执行。
  • 第二步,协程 A 执行到一半,进入暂停,执行权转移到协程 B。
  • 第三步,协程 B 交换执行权。
  • 第四步,协程 A 恢复执行。 上面流程的协程 A,就是异步任务,分成两段执行。

异步读取文件路径例子如下。

function* asyncReadFile() {
  ...
  let file = yield readFile(fileA)
  ...
}

上面代码函数asyncReadFile是一个协程,yield命令表示执行到此处,执行权转交给其他协程,等执行权返回,再从暂停的地方继续往后执行。最大的优点,就是代码的写法非常像同步操作。

特征

形式上 ,Generator函数是一个普通函数,但有两个特征。

  • function与函数名之间有个*
  • 函数体内可用yield语句定义不同的内部状态。

基本使用:

function* generator() {
  yield 'hello'
  yield 'world'
  return 'hello world'
}

let iterator = generator()
iterator.next() // {value: "hello", done: false}
iterator.next() // {value: "world", done: false}
iterator.next() // {value: "hello world", done: true}
iterator.next() // {value: undefined, done: true}

调用Generator函数返回一个iterator迭代器对象,代表Generator函数的内部指针。每调用iterator对象的next方法,就会返回一个带有valuedone属性的对象,value代表当前内部状态值,done表示遍历是否结束。

yield表达式

yieldGenerator函数的暂停标志, 仅在Generator函数中使用,用在其他地方会报错。

(function (){
  yield 1;
})()
// SyntaxError: Unexpected number

yield 表达式如果用在另一个表达式中,必须放在圆括号里面。

function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}

yield 表达式用作参数或放在赋值表达式的右边,可以不加括号。

function* demo() {
 foo(yield 'a', yield 'b'); // OK
 let input = yield; // OK
}

yield* 表达式

在默认情况下,调用Generator函数返回的是迭代对象,所以在一个Generator函数中调用另一个Generator函数无法打印出调用Generator中的值。

function* foo() {
  yield 'a'
  yield 'b'
}

function* bar() {
  foo() // 是迭代对象
  yield 'c'
  yield 'd'
}

let iterator = bar()

for(let value of iterator) {
  console.log(value)
}
// c
// d

可稍微改动下。

function* foo() {
  yield 'a'
  yield 'b'
}

function* bar() {
  let iterator = foo() 
  for(let i of iterator) yield i
  yield 'c'
  yield 'd'
}

let iterator = bar()

for(let value of iterator) {
  console.log(value)
}
// a
// b
// c
// d

当然使用yield*方法更简单。

function* foo() {
  yield 'a'
  yield 'b'
}

function* bar() {
  yield* foo()
  yield 'c'
  yield 'd'
}

let iterator = bar()

for(let value of iterator) {
  console.log(value)
}
// a
// b
// c
// d

Generator函数用法,大致如此。那async/awaitGenerator函数还有哪些不同?

  • async/await自带执行器,不需要手动next就能自动执行下一步。
  • async函数的返回值是Promise对象,Generator函数返回的是生成器对象。
  • await函数返回的是Promiseresolve/reject的值。 接下来,就是针对以上三点对Generator函数的封装。

async/await 函数

async 函数的实现原理,就是将Generator函数和自动执行器,包装在一个函数里。await则替换yield关键字。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  })
}

其中spawn函数就是自动执行器。Promisethen是自动执行的关键所在。

function spawn(genF) {
 // 返回值是promise
 return new Promise(function(resolve, reject) {
    const gen = genF();  // 获取最新的迭代器
    function step(nextF) {
      let next;
      // 错误处理
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      // res.value包装为 promise,兼容 yield 后基本类型的情况
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

参考

《ES6 标准入门》
你不知道的Javascript(下卷)
Async / Await / Generator 实现原理