Babel 是如何转换 async/await 的?

一切的一切都要从 regeneratorRuntime is not defined 报错开始说起,在我们使用 es7 语法async/await的时候,常常会利用 babel 进行兼容处理,在 babel 处理的过程中,如果useBuiltIns(useBuiltIns 是配置 babel 如何处理 polyfills 的)设置为entry(该设置为在入口处根据 target 引入 polyfills),并且没有引用插件 @babel/plugin-transform-runtime就会生成这个报错。

我们抛开 babel 的配置不说,从头看看这个 regeratorRuntime 是个什么东西。为什么兼容 async 和 await 的时候会使用这个东西。

从异步编程开始说起

js 的异步编程从远古开始说起当然就是回调函数啦:假设 step1/step2/step3 都 return promise

  • 1.0.石器时代:
 !function(){
     step1('options', function(res1) {
         step2('options', function(res2) {
             step3('options', function(res3) {
                 ......
             })
         })
     })
 }()

这样就会造成 callback hell。

  • 2.0.青铜时代:promise 的链式调用
step1()
  .then((res1) => {
    // do sth
    return step2();
  })
  .then((res2) => {
    //do sth
    return step3();
  })
  .then((res3) => {
    //do sth
  });
  • 3.0.现代 async/await:
async () => {
  const res1 = await step1();
  const res2 = await step2();
  const res3 = await step3();
};
  • 2.5.但其实在青铜时代和现代之间还出来了一个蒸汽时代,但是由于现代来的太快,马上就取代了蒸汽时代的产物:Generator
function* stepBystep() {
  yield step1();
  yield step2();
  yield step3();
}

const pipeline = stepBystep();
let res1 = pipeline.next(); // {done: false, value: step1的返回值}
let res2 = pipeline.next(); // {done: false, value: step2的返回值}
let res3 = pipeline.next(); // {done: true, value: step3的返回值}

yield有投降/屈服的意思,感觉就是“让出现在的执行权利”的意思。
自此我们比较一下2.53,0使用方法,感觉很相似,但是有几个不同点:
1.async/await 在执行完毕之后会自动往下执行,但是 generator 会停止等待手动调用next()方法继续执行。(自动挡和手动挡) 2.如果你打印两者的 res1/res2/res3 话会发现在3.0时代打印出来的是 promise,2.5时代打印出来的是生成器对象

await/async = generator + promise

正如我们上面所说的,async/await 和 generator 的主要区别之一就是一个是自动挡,一个是手动挡,那么只要给 generator 加上一个自动换挡系统,那是不是在流程上就可以和 async/await 一样了?我们利用 promise 的 resolve 来充当这个自动挡系统: 还是上面的例子:

function* stepBystep() {
  let step1 = yield Promise.resolve("step1"); // step1
  console.log(step1);
  let step2 = yield Promise.resolve("step2"); // step2
  console.log(step2);
  let step3 = yield Promise.resolve("step3"); // step3
  console.log(step3);
}

// 自动挡系统添加函数如下:
function automaticRun(generator) {
  let pipeline = generator();
  let _next = (val) => {
    let res = pipeline.next(val);
    if (res.done) {
      return res.value;
    } else {
      // 在每个step的then中执行下一步
      res.value.then((val) => {
        console.log(val);
        _next(val); //next可以携带一个参数,可以作为在generator中上一个yeild(暂停的那个)表达式的返回值
      });
    }
  };
  _next();
}

automaticRun(stepBystep);

// automaticRun等于做了下面的这些事情:-----------------------
var manual = stepBystep();
let start = manual.next(); // 执行第一个yield,此时start是Promise.resolve('step1')的返回值
start.value.then((val) => {
  let res1 = manual.next(val); // 执行第一个yield和第二个yield之间的方法,并将val设置为第一个yield的返回值
  res1.value.then((val1) => {
    let res2 = manual.next(val1); // 执行第二个yield和第三个yield之间的方法,并将val1设置为第二个yield的返回值
    res2.value.then((val2) => {
      let res3 = manual.next(val2); // 执行第三个yield到函数结束之前方法,并将val2设置为第三个yield的方法
    });
  });
});

正如上面的代码,我们利用 promise 和 generator 实现了类似于 await/async 的语法。感兴趣的同学可以去babel try it out写个 async/await 看一下 babel 的处理和我们实现的版本思路一致,只是做了额外的几点:

  1. 因为 async 最终返回的是 promise,所以最终会用 promise 包裹一层
  2. 我们直接假设 yeild(await)后面(res.value)一定跟着 promise,但其实他也可能不是,所以也需要用 promise 包裹一层
  3. 处理 generator 的 throw 和 catch promise 的异常。

regeneratorRuntime = babel 对 generator 的兼容处理

终于来到了这个报错,regeneratorRuntime 是什么呢?上一节的总结是async/await = generator + promise又因为 generator 是 es6 才支持的 feature,所以 babel 同时也需要对 generator 进行兼容。。。
举个例子:

async function test() {
  var result1 = await timer("result1");
  var result2 = await timer("result2");
  var result3 = await timer("result3");
  return result1 + result2 + result3;
}

let timer = (value) => {
  // 模拟一个简单的异步操作
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(value);
    }, 2000);
  });
};

test().then((res) => console.log(res)); //大约在6s之后输出 result1result2result3

babel try it out得到 babel 输出的结果如下:

function _asyncToGenerator(fn) {
  // 此处省略,这里相当于上面我们自制的automaticRun的方法
}

function test() {
  return _test.apply(this, arguments);
}

function _test() {
  _test = _asyncToGenerator(
    /*#__PURE__*/ regeneratorRuntime.mark(function _callee() {
      var result1, result2, result3;
      return regeneratorRuntime.wrap(function _callee$(_context) {
        // 省略。。
      }, _callee);
    })
  );
  return _test.apply(this, arguments);
}

var timer = function timer(value) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(value);
    }, 2000);
  });
};

test().then(function (res) {
  return console.log(res);
});

_asyncToGenerator相当于我们上面自己实现的‘自动挡’函数,在函数中我们又看到了不知道哪来的regeneratorRuntime.markregeneratorRuntime.wrap,又因为generator其实是 es6 标准,如果你利用 babel 兼容 es5 语法的话,也就需要对generator做语法兼容(其实感觉现在很少有不支持 es6 的情况了)。所以regeneratorRuntime就是实现了一个 generator。这里附上regenratorRuntime 的完整实现整个实现过程很复杂,我们提炼出一个简单版的 Generator 实现过程。

Generator 模拟实现

先从结果出发,我们要完成的效果如下

// 如果定义的generator如下:
function* stepBystep() {
  yield "result1";
  yield "result2";
  yield "result3";
}

// 那么使用的效果就该是这样:
const pipeline = stepBystep();
let res1 = pipeline.next(); // {done: false, value: 'result1'}
let res2 = pipeline.next(); // {done: false, value: 'result2'}
let res3 = pipeline.next(); // {done: false, value: 'result3'}
let res4 = pipeline.next(); // {done: true, value: undefined}
  1. 首先是执行stepBystep函数,得到一个带 next 方法的 generator,并且 next 返回一个 object
    function stepBystep() {
      return {
        next: function() {
            return {
                value: null
                done: null
            }
        }
      }
    }
  1. 因为每次执行完一次 next(),需要记录当下的执行情况,所以我们还需要定义一个 context 来记录现在的执行情况
    function execution() {
        //执行下一步
    }

    var context = {
        step: 0
        done: false
    }

    function stepBystep() {
      return {
        next: function() {
            return {
                value:  context.done ? undefined : execution(context)
                done: context.done
            }
        }
      }
    }
  1. execution 方法来执行每一步并确定输出值
function execution(_context) {
  while (1) {
    switch (_context.step) {
      case 0:
        _context.step = 2;
        return "result1";

      case 2:
        _context.step = 4;
        return "result2";

      case 4:
        _context.step = 6;
        return "result3";

      case 6:
        _context.stop();
        return undefined;
    }
  }
}

function stepBystep() {
  var context = {
    step: 0,
    done: false,
    stop: function stop() {
      this.done = true;
    },
  };

  return {
    next: function () {
      return {
        value: context.done ? undefined : execution(context),
        done: context.done,
      };
    },
  };
}
//最终输出如下:
const pipeline = stepBystep();
let res1 = pipeline.next(); // {done: false, value: 'result1'}
let res2 = pipeline.next(); // {done: false, value: 'result2'}
let res3 = pipeline.next(); // {done: false, value: 'result3'}
let res4 = pipeline.next(); // {done: true, value: 'undefined'}

以上就是 regeratorRuntime 的超级简单版本,regeneratorRuntime.mark 对应的是在原型链上添加对象;regeneratorRuntime.wrap 做的事情就是我们上面所做的事情,将创建出来的 generator 添加 next 方法,创建 context 保存执行状态,最终模拟出一个 generator。

总结:

  1. generatorRuntime 是在@babel/plugin-transform-runtime插件中(旧版本是@babel/polyfill)中的内容,用作给 generator 做语法兼容。
  2. await/async 语法的兼容是利用 generator 和 promise 实现的,给手动挡汽车(generator)配上一个自动变速箱(promise)。

references:

  1. https://zhuanlan.zhihu.com/p/131389852
  2. https://juejin.cn/post/6902324810748002311#heading-12