Babel 是如何转换 async/await 的?

babel Mar 18, 2021

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

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

Tags

super_haochen

Peace && love

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.