Async / Await / Generator 实现原理

async/await实现

在多个回调依赖的场景中,尽管Promise经过链式调用取代了回调嵌套,但过多的链式调用可读性仍然不佳,流程控制也不方便,ES7 提出的async 函数,终于让 JS 对于异步操做有了终极解决方案,简洁优美地解决了以上两个问题。javascript

设想一个这样的场景,异步任务a->b->c之间存在依赖关系,若是咱们经过then链式调用来处理这些关系,可读性并非很好。java

若是咱们想控制其中某个过程,好比在某些条件下,b不往下执行到c,那么也不是很方便控制。编程

Promise.resolve(a)
  .then(b => {
    // do something
  })
  .then(c => {
    // do something
  })

可是若是经过async/await来实现这个场景,可读性和流程控制都会方便很多。promise

async () => {
  const a = await Promise.resolve(a);
  const b = await Promise.resolve(b);
  const c = await Promise.resolve(c);
}

那么咱们要如何实现一个async/await呢,首先咱们要知道,async/await其实是对Generator(生成器)的封装,是一个语法糖。babel

因为Generator出现不久就被async/await取代了,不少同窗对Generator比较陌生,所以咱们先来看看Generator的用法:app

ES6 新引入了 Generator 函数,能够经过 yield 关键字,把函数的执行流挂起,经过next()方法能够切换到下一个状态,为改变执行流程提供了可能,从而为异步编程提供解决方案。异步

function* myGenerator() {
  yield '1'
  yield '2'
  return '3'
}

const gen = myGenerator();  // 获取迭代器
gen.next()  //{value: "1", done: false}
gen.next()  //{value: "2", done: false}
gen.next()  //{value: "3", done: true}

也能够经过给next()传参, 让yield具备返回值async

function* myGenerator() {
  console.log(yield '1')  //test1
  console.log(yield '2')  //test2
  console.log(yield '3')  //test3
}

// 获取迭代器
const gen = myGenerator();

gen.next()
gen.next('test1')
gen.next('test2')
gen.next('test3')

咱们看到Generator的用法,应该️会感到很熟悉,*/yield和async/await看起来其实已经很类似了,它们都提供了暂停执行的功能,但两者又有三点不一样:异步编程

  • async/await自带执行器,不须要手动调用next()就能自动执行下一步
  • async函数返回值是Promise对象,而Generator返回的是生成器对象
  • await可以返回Promise的resolve/reject的值

咱们对async/await的实现,其实也就是对应以上三点封装Generator。函数

自动执行

咱们先来看一下,对于这样一个Generator,手动执行是怎样一个流程。

function* myGenerator() {
  yield Promise.resolve(1);
  yield Promise.resolve(2);
  yield Promise.resolve(3);
}

// 手动执行迭代器
const gen = myGenerator()
gen.next().value.then(val => {
  console.log(val)
  gen.next().value.then(val => {
    console.log(val)
    gen.next().value.then(val => {
      console.log(val)
    })
  })
})

//输出1 2 3

咱们也能够经过给gen.next()传值的方式,让yield能返回resolve的值。

function* myGenerator() {
  console.log(yield Promise.resolve(1))   //1
  console.log(yield Promise.resolve(2))   //2
  console.log(yield Promise.resolve(3))   //3
}

// 手动执行迭代器
const gen = myGenerator()
gen.next().value.then(val => {
  // console.log(val)
  gen.next(val).value.then(val => {
    // console.log(val)
    gen.next(val).value.then(val => {
      // console.log(val)
      gen.next(val)
    })
  })
})

显然,手动执行的写法看起来既笨拙又丑陋,咱们但愿生成器函数能自动往下执行,且yield能返回resolve的值。

基于这两个需求,咱们进行一个基本的封装,这里async/await是关键字,不能重写,咱们用函数来模拟:

function run(gen) {
  var g = gen()                     //因为每次gen()获取到的都是最新的迭代器,所以获取迭代器操做要放在_next()以前,不然会进入死循环

  function _next(val) {             //封装一个方法, 递归执行g.next()
    var res = g.next(val)           //获取迭代器对象,并返回resolve的值
    if(res.done) return res.value   //递归终止条件
    res.value.then(val => {         //Promise的then方法是实现自动迭代的前提
      _next(val)                    //等待Promise完成就自动执行下一个next,并传入resolve的值
    })
  }
  _next()  //第一次执行
}

对于咱们以前的例子,咱们就能这样执行:

function* myGenerator() {
  console.log(yield Promise.resolve(1))   //1
  console.log(yield Promise.resolve(2))   //2
  console.log(yield Promise.resolve(3))   //3
}

run(myGenerator)

这样咱们就初步实现了一个async/await。

上边的代码只有五六行,但并非一下就能看明白的,咱们以前用了四个例子来作铺垫,也是为了让读者更好地理解这段代码。 

简单来讲,咱们封装了一个run方法,run方法里咱们把执行下一步的操做封装成_next(),每次Promise.then()的时候都去执行_next(),实现自动迭代的效果。

在迭代的过程当中,咱们还把resolve的值传入gen.next(),使得yield得以返回Promise的resolve的值

这里插一句,是否是只有.then方法这样的形式才能完成咱们自动执行的功能呢?答案是否认的,yield后边除了接Promise,还能够接thunk函数,thunk函数不是一个新东西,所谓thunk函数,就是单参的只接受回调的函数。

不管是Promise仍是thunk函数,其核心都是经过传入回调的方式来实现Generator的自动执行。thunk函数只做为一个拓展知识,理解有困难的同窗也能够跳过这里,并不影响后续理解。

返回Promise & 异常处理

虽然咱们实现了Generator的自动执行以及让yield返回resolve的值,但上边的代码还存在着几点问题:

  • 须要兼容基本类型:这段代码能自动执行的前提是yield后面跟Promise,为了兼容后面跟着基本类型值的状况,咱们须要把yield跟的内容(gen().next.value)都用Promise.resolve()转化一遍
  • 缺乏错误处理:上边代码里的Promise若是执行失败,就会致使后续执行直接中断,咱们须要经过调用Generator.prototype.throw(),把错误抛出来,才能被外层的try-catch捕获到
  • 返回值是Promise:async/await的返回值是一个Promise,咱们这里也须要保持一致,给返回值包一个Promise

咱们改造一下run方法:

function run(gen) {
  //把返回值包装成promise
  return new Promise((resolve, reject) => {
    var g = gen()

    function _next(val) {
      //错误处理
      try {
        var res = g.next(val) 
      } catch(err) {
        return reject(err); 
      }
      if(res.done) {
        return resolve(res.value);
      }
      //res.value包装为promise,以兼容yield后面跟基本类型的状况
      Promise.resolve(res.value).then(
        val => {
          _next(val);
        }, 
        err => {
          //抛出错误
          g.throw(err)
        });
    }
    _next();
  });
}

而后咱们能够测试一下:

function* myGenerator() {
  try {
    console.log(yield Promise.resolve(1)) 
    console.log(yield 2)   //2
    console.log(yield Promise.reject('error'))
  } catch (error) {
    console.log(error)
  }
}

const result = run(myGenerator)     //result是一个Promise
//输出 1 2 error

到这里,一个async/await的实现基本完成了。最后咱们能够看一下babel对async/await的转换结果,其实总体的思路是同样的,可是写法稍有不一样:

//至关于咱们的run()
function _asyncToGenerator(fn) {
  // return一个function,和async保持一致。咱们的run直接执行了Generator,实际上是不太规范的
  return function() {
    var self = this
    var args = arguments
    return new Promise(function(resolve, reject) {
      var gen = fn.apply(self, args);

      //至关于咱们的_next()
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
      }
      //处理异常
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
      }
      _next(undefined);
    });
  };
}

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw);
  }
}

使用方式:

const foo = _asyncToGenerator(function* () {
  try {
    console.log(yield Promise.resolve(1))   //1
    console.log(yield 2)                    //2
    return '3'
  } catch (error) {
    console.log(error)
  }
})

foo().then(res => {
  console.log(res)                          //3
})

有关async/await的实现,到这里就告一段落了。可是直到结尾,咱们也不知道await究竟是如何暂停执行的,有关await暂停执行的秘密,咱们还要到Generator的实现中去寻找答案。

Generator实现

咱们从一个简单的Generator使用实例开始,一步步探究Generator的实现原理:

function* foo() {
  yield 'result1'
  yield 'result2'
  yield 'result3'
}
  
const gen = foo()
console.log(gen.next().value)
console.log(gen.next().value)
console.log(gen.next().value)

咱们能够在babel官网上在线转化这段代码,看看ES5环境下是如何实现Generator的:

"use strict";

var _marked =
/*#__PURE__*/
regeneratorRuntime.mark(foo);

function foo() {
  return regeneratorRuntime.wrap(function foo$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 'result1';

        case 2:
          _context.next = 4;
          return 'result2';

        case 4:
          _context.next = 6;
          return 'result3';

        case 6:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

var gen = foo();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);

代码咋一看不长,但若是仔细观察会发现有两个不认识的东西 —— regeneratorRuntime.mark和regeneratorRuntime.wrap,这二者实际上是 regenerator-runtime 模块里的两个方法。

regenerator-runtime 模块来自facebook的 regenerator 模块,完整代码在runtime.js,这个runtime有700多行...-_-||,所以咱们不能全讲,不过重要的部分咱们就简单地过一下,重点讲解暂停执行相关部分代码。

我的以为啃源码的效果不是很好,建议读者拉到末尾先看结论和简略版实现,源码做为一个补充理解。

regeneratorRuntime.mark()

regeneratorRuntime.mark(foo)这个方法在第一行被调用,咱们先看一下runtime里mark()方法的定义。

//runtime.js里的定义稍有不一样,多了一些判断,如下是编译后的代码
runtime.mark = function(genFun) {
  genFun.__proto__ = GeneratorFunctionPrototype;
  genFun.prototype = Object.create(Gp);
  return genFun;
};

这里边GeneratorFunctionPrototype和Gp咱们都不认识,他们被定义在runtime里,不过不要紧,咱们只要知道mark()方法为生成器函数(foo)绑定了一系列原型就能够了,这里就简单地过了。

regeneratorRuntime.wrap()

从上面babel转化的代码咱们能看到,执行foo(),其实就是执行wrap(),那么这个方法起到什么做用呢,他想包装一个什么东西呢,咱们先来看看wrap方法的定义:

//runtime.js里的定义稍有不一样,多了一些判断,如下是编译后的代码
function wrap(innerFn, outerFn, self) {
  var generator = Object.create(outerFn.prototype);
  var context = new Context([]);
  generator._invoke = makeInvokeMethod(innerFn, self, context);

  return generator;
}

wrap方法先是建立了一个generator,并继承outerFn.prototype;而后new了一个context对象;makeInvokeMethod方法接收innerFn(对应foo$)、context和this,并把返回值挂到generator._invoke上;最后return了generator。

其实wrap()至关因而给generator增长了一个_invoke方法。

这段代码确定让人产生不少疑问,outerFn.prototype是什么,Context又是什么,makeInvokeMethod又作了哪些操做。下面咱们就来一一解答:

outerFn.prototype其实就是genFun.prototype

这个咱们结合一下上面的代码就能知道

context能够直接理解为这样一个全局对象,用于储存各类状态和上下文:

var ContinueSentinel = {};

var context = {
  done: false,
  method: "next",
  next: 0,
  prev: 0,
  abrupt: function(type, arg) {
    var record = {};
    record.type = type;
    record.arg = arg;

    return this.complete(record);
  },
  complete: function(record, afterLoc) {
    if (record.type === "return") {
      this.rval = this.arg = record.arg;
      this.method = "return";
      this.next = "end";
    }

    return ContinueSentinel;
  },
  stop: function() {
    this.done = true;
    return this.rval;
  }
};

makeInvokeMethod的定义以下,它return了一个invoke方法,invoke用于判断当前状态和执行下一步,其实就是咱们调用的next()

//如下是编译后的代码
function makeInvokeMethod(innerFn, context) {
  // 将状态置为start
  var state = "start";

  return function invoke(method, arg) {
    // 已完成
    if (state === "completed") {
      return { value: undefined, done: true };
    }
    
    context.method = method;
    context.arg = arg;

    // 执行中
    while (true) {
      state = "executing";

      var record = {
        type: "normal",
        arg: innerFn.call(self, context)    // 执行下一步,并获取状态(其实就是switch里边return的值)
      };

      if (record.type === "normal") {
        // 判断是否已经执行完成
        state = context.done ? "completed" : "yield";

        // ContinueSentinel实际上是一个空对象,record.arg === {}则跳过return进入下一个循环
        // 何时record.arg会为空对象呢, 答案是没有后续yield语句或已经return的时候,也就是switch返回了空值的状况(跟着上面的switch走一下就知道了)
        if (record.arg === ContinueSentinel) {
          continue;
        }
        // next()的返回值
        return {
          value: record.arg,
          done: context.done
        };
      }
    }
  };
}

为何generator._invoke实际上就是gen.next呢,由于在runtime对于next()的定义中,next()其实就return了_invoke方法

// Helper for defining the .next, .throw, and .return methods of the
// Iterator interface in terms of a single ._invoke method.
function defineIteratorMethods(prototype) {
    ["next", "throw", "return"].forEach(function(method) {
      prototype[method] = function(arg) {
        return this._invoke(method, arg);
      };
    });
}

defineIteratorMethods(Gp);

低配实现 & 调用流程分析

这么一遍源码下来,估计不少读者仍是懵逼的,毕竟源码中纠集了不少概念和封装,一时半会很差彻底理解,让咱们跳出源码,实现一个简单的Generator,而后再回过头看源码,会获得更清晰的认识。

// 生成器函数根据yield语句将代码分割为switch-case块,后续经过切换_context.prev和_context.next来分别执行各个case
function gen$(_context) {
  while (1) {
    switch (_context.prev = _context.next) {
      case 0:
        _context.next = 2;
        return 'result1';

      case 2:
        _context.next = 4;
        return 'result2';

      case 4:
        _context.next = 6;
        return 'result3';

      case 6:
      case "end":
        return _context.stop();
    }
  }
}

// 低配版context  
var context = {
  next:0,
  prev: 0,
  done: false,
  stop: function stop () {
    this.done = true
  }
}

// 低配版invoke
let gen = function() {
  return {
    next: function() {
      value = context.done ? undefined: gen$(context)
      done = context.done
      return {
        value,
        done
      }
    }
  }
} 

// 测试使用
var g = gen() 
g.next()  // {value: "result1", done: false}
g.next()  // {value: "result2", done: false}
g.next()  // {value: "result3", done: false}
g.next()  // {value: undefined, done: true}

这段代码并不难理解,咱们分析一下调用流程:

  • 咱们定义的function*生成器函数被转化为以上代码
  • 转化后的代码分为三大块:
  1. gen$(_context)由yield分割生成器函数代码而来
  2. context对象用于储存函数执行上下文
  3. invoke()方法定义next(),用于执行gen$(_context)来跳到下一步
  • 当咱们调用g.next(),就至关于调用invoke()方法,执行gen$(_context),进入switch语句,switch根据context的标识,执行对应的case块,return对应结果
  • 当生成器函数运行到末尾(没有下一个yield或已经return),switch匹配不到对应代码块,就会return空值,这时g.next()返回{value: undefined, done: true}

从中咱们能够看出,Generator实现的核心在于上下文的保存,函数并无真的被挂起,每一次yield,其实都执行了一遍传入的生成器函数,只是在这个过程当中间用了一个context对象储存上下文,使得每次执行生成器函数的时候,均可以从上一个执行结果开始执行,看起来就像函数被挂起了同样。

相关文章
相关标签/搜索