bable如何转换async/await

2,678 阅读6分钟

背景

async/await是定义于ES2017的异步语法,async/await语法大大提高了异步JS代码的可读性,帮助JS开发者进一步摆脱了臭名昭著的回调地狱。

包括Edge(15+),Chrome(55+),Safari/iOS Safari(11+)在内的主流浏览器都支持了这一语法。不过在平时的开发中我们通常会用Bable来对这一语法进行转换。

那么Babel会怎么转换与执行async/await呢?

示例代码

编写以下代码,并通过Webpack+Babel进行编译(vue cli 3默认配置,经过plugin-transform-async-to-generatorregenerator-transform转换):

const getThree = function() {
  return Promise.resolve(3);
};
const getFour = function() {
  return Promise.resolve(4);
};

const test = async function() {
  console.log(1);
  console.log(2);
  const three = await getThree();
  console.log(three);
  const four = await getFour();
  console.log(four);
  console.log(5);
};

将得到如下的代码:

var test = /*#__PURE__*/function () {
  var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
    var three, four;
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            console.log(1);
            console.log(2);
            _context.next = 4;
            return getThree();

          case 4:
            three = _context.sent;
            console.log(three);
            _context.next = 8;
            return getFour();

          case 8:
            four = _context.sent;
            console.log(four);
            console.log(5);

          case 11:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));

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

接下来,将以以上代码为例,从运行时和babel转换阶段分别介绍async/await的奇幻漂流。

运行时

regeneratorRuntime.mark

regeneratorRuntime.mark会把_callee的prototype设置为Generator

exports.mark = function(genFun) {
  if (Object.setPrototypeOf) {
    Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);
  } else {
    genFun.__proto__ = GeneratorFunctionPrototype;
    if (!(toStringTagSymbol in genFun)) {
      genFun[toStringTagSymbol] = "GeneratorFunction";
    }
  }
  genFun.prototype = Object.create(Gp);
  return genFun;
};

Generator.prototype有next、return、throw三个方法,这三个方法会分别以'next'、'return'和'throw'调用this._invoke(method, arg)。这里的_invoke是什么之后会介绍。

regeneratorRuntime.wrap

regeneratorRuntime.wrap的作用是生成一个Generator实例,返回的generator的_invoke方法是由_callee$生成的:

function wrap(innerFn, outerFn, self, tryLocsList) {
  var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator;
  var generator = Object.create(protoGenerator.prototype);
  var context = new Context(tryLocsList || []);

  generator._invoke = makeInvokeMethod(innerFn, self, context);

  return generator;
}

这里innerFn就是_callee$,outerFn是_callee。(所以regeneratorRuntime.mark这步是非必要的?)

makeInvokeMethod

makeInvokeMethod包装了innerFn(状态机)的执行流程控制,外部通过next等方法操作状态机:

function makeInvokeMethod(innerFn, self, context) {
  var state = GenStateSuspendedStart;

  return function invoke(method, arg) {
    ...

    context.method = method;
    context.arg = arg;

    while (true) {
      ...

      if (context.method === "next") {
        context.sent = context._sent = context.arg;
      } else if (context.method === "throw") {
        if (state === GenStateSuspendedStart) {
          state = GenStateCompleted;
          throw context.arg;
        }

        context.dispatchException(context.arg);
      } else if (context.method === "return") {
        context.abrupt("return", context.arg);
      }

      state = GenStateExecuting;

      var record = tryCatch(innerFn, self, context);
      if (record.type === "normal") {
        state = context.done
          ? GenStateCompleted
          : GenStateSuspendedYield;

        if (record.arg === ContinueSentinel) {
          continue;
        }

        return {
          value: record.arg,
          done: context.done
        };

      } else if (record.type === "throw") {
        state = GenStateCompleted;
        context.method = "throw";
        context.arg = record.arg;
      }
    }
  };
}

状态机(_callee$)的初始状态为GenStateSuspendedStart。随后调用next执行状态机。methodnext之类的操作,arg是参数,即初始的调用参数或状态机上一步的返回。context是状态机的上下文,arg被赋值到context.sent以供状态机获取,状态机的prevnext是控制状态机的跳转,初始都为0,在状态机中会自行修改。

当状态机return时,这里会判断状态机状态是否完成,状态机中的_context.stop()会将context.done设置为true。

如果原始代码里有await(yield),那状态机中就会生成对应的return代码将Promise返回出去。之后_asyncToGenerator会保证异步流程的继续执行。

_asyncToGenerator

_asyncToGenerator将Generator或状态机包装为Promise返回,同时通过asyncGeneratorStep使得异步流程持续执行:

function _asyncToGenerator(fn) {
  return function () {
    var self = this,
        args = arguments;
    return new Promise(function (resolve, reject) {
      var gen = fn.apply(self, args);

      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }

      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }

      _next(undefined);
    });
  };
}

asyncGeneratorStep会驱动Generator或状态机不断前进,每次返回后,判断返回类型,如果是异步流程(Promise)则等待Promise有结果后继续处理:

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);
  }
}

小结

为了更便于理解,这里简述一下执行流程:

    1. test方法被调用,实际调用的是_asyncToGenerator返回的function,这个function返回的是一个promise,同时调用asyncGeneratorStep开始执行
    1. asyncGeneratorStep首先调用generator的next方法,这里的generator的_invoke方法会执行状态机_callee
    1. 状态机初次执行case 0,执行完后状态变成{ done: false, next: 4 }并返回getThree()
    1. asyncGeneratorStep在getThree()完成后将结果3作为context.sent继续调用状态机
    1. 状态机从case 4执行,从context.sent获取上个异步流程的结果
    1. 重复4-5这样的步骤,知道执行完毕

以上,就是async/await装换后的代码的执行流程。

转换阶段

在我的webpack配置中(vue cli 3默认配置),async/await的转换经历了两个插件:plugin-transform-async-to-generatorregenerator-transform

plugin-transform-async-to-generator

plugin-transform-async-to-generator的工作相对简单,让首先看下转换的结果:

var test = /*#__PURE__*/function () {
  var _ref = _asyncToGenerator(function* () {
    console.log(1);
    console.log(2);
    var three = yield getThree();
    console.log(three);
    var four = yield getFour();
    console.log(four);
    console.log(5);
  });

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

原本的代码被转换为generator,async/await分别对应了*与yield,并最终通过_asyncToGenerator转换为Promise并完成代码异步流程的执行。

这里的逻辑比较直观,感兴趣的话可以自己阅读源码(需要先对AST、babel、acorn等有所了解)。

regenerator-transform

regenerator-transform完成的工作是将generator(或async)代码转换为状态机。

完整的代码涉及了各种语法节点例如switch case、for loop等的转换处理逻辑,这里将仅仅以示例代码中涉及的部分介绍大体的流程。

// visit.js
exports.getVisitor = ({ types: t }) => ({
  Function: {
    exit: util.wrapWithTypes(t, function(path, state) {
      let node = path.node;

      if (!shouldRegenerate(node, state)) return;

      ...

      let emitter = new Emitter(contextId);
      emitter.explode(path.get("body"));

      ...
    })
  }
});

visit.js添加了Function的访问器。shouldRegenerate判断当前function节点是否为generator或async,如果不是,则不对节点做改动;如果是,则通过emitter.explode将原代码段转换为状态机代码。

// emitter.js

Ep.explode = function(path, ignoreResult) {
  ...

  if (t.isStatement(node))
    return self.explodeStatement(path);

  if (t.isExpression(node))
    return self.explodeExpression(path, ignoreResult);

  ...
};

示例代码的代码体是一个BlockStatement节点,因此进入explodeStatement方法:

// emitter.js

Ep.explodeStatement = function (path, labelId) {
  ...

  if (t.isBlockStatement(stmt)) {
    path.get("body").forEach(function (path) {
      self.explodeStatement(path);
    });
    return;
  }

  if (!meta.containsLeap(stmt)) {
    self.emit(stmt);
    return;
  }

  switch (stmt.type) {
    case "ExpressionStatement":
      self.explodeExpression(path.get("expression"), true);
      break;
    
    ...
  }
}

如前所述,根节点为BlockStatement,因此方法将遍历其子节点,调用explodeStatement方法。

示例代码中,根节点的每个子节点都是ExpressionStatement,因此我们就跳过例如IfStatementWhileStatementSwitchStatement等等各类节点的处理分析。

对于每个节点,将通过containsLeap判断其是否需要特别处理:

// meta.js

{
  YieldExpression: true,
  BreakStatement: true,
  ContinueStatement: true,
  ReturnStatement: true,
  ThrowStatement: true
}

如果不需要特别处理,则直接调用emit方法:

// emitter.js

Ep.emit = function (node) {
  ...

  this.listing.push(node);
};

例如示例中的console.log(1)console.log(2)两行代码,都直接加入了列表,因此在转换后的代码中,这两行代码仍然是在一起的,并不需要插入return或者_context.next =等控制代码,原代码流程保持不变。

而对于await(已经转换为了yield)的代码行,将需要额外处理,加入状态机的控制代码:

Ep.explodeExpression = function(path, ignoreResult) {
  ...

  function finish(expr) {
    ...
    
    self.emit(expr);
  }

  if (!meta.containsLeap(expr)) {
    return finish(expr);
  }

  switch (expr.type) {
    ...
  }
}

这里依然是使用了meta.containsLeap来判断是否需要处理,如果不需要,则直接调用emit

我们来看进入explodeExpression的代码行const three = await getThree(),这是一个需要进一步处理的AssignmentExpression,处理方式是进一步处理赋值的左右部分:

case "AssignmentExpression":
  if (expr.operator === "=") {
    return finish(t.assignmentExpression(
      expr.operator,
      self.explodeExpression(path.get("left")),
      self.explodeExpression(path.get("right"))
    ));
  }
  
  ...

进一步分解这个语句:

  • const three是一个Identifier,无需处理
  • await getThree()是一个需要进一步处理的AssignmentExpression
  • getThree()是一个CallExpression,无需进一步处理
case "YieldExpression":
  after = this.loc();
  let arg = expr.argument && self.explodeExpression(path.get("argument"));

  ...

  self.emitAssign(self.contextProperty("next"), after);

  let ret = t.returnStatement(t.cloneDeep(arg) || null);

  ret.loc = expr.loc;
  self.emit(ret);
  self.mark(after);

  return self.contextProperty("sent");

这里loc()获取的是当前行号(当前代码块中的行号),也就是在示例代码转换结果中看到的case 4case 8中的4和8。emitAssign生成了_context.next = 4这样的状态跳转代码,emit(ret)这步生成了return getFour()这些代码,用以将控制流程转回到_asyncToGeneratorcontextProperty("sent")指明了异步流程返回值的获取方式four = _context.sent

mark方法标记了行号,以便在节点转换完成后统一插入case代码:

Ep.mark = function(loc) {
  let index = this.listing.length;

  ...

  this.marked[index] = true;
  return loc;
};

Ep.getDispatchLoop = function () {
  ...

  let alreadyEnded = false;
  self.listing.forEach(function(stmt, i) {
    if (self.marked.hasOwnProperty(i)) {
      cases.push(t.switchCase(t.numericLiteral(i), current = []));
      alreadyEnded = false;
    }

    if (!alreadyEnded) {
      current.push(stmt);
      if (t.isCompletionStatement(stmt)) alreadyEnded = true;
    }
  });

  ...
};

小结

至此,简单介绍了async/await语法在被babel插件处理成状态机语法的过程,以及运行时状态机的执行流程。

感兴趣的话可以阅读regenerator-transform的源码,了解各种语法在从异步语法转换为状态机过程中的流程。

谢谢您的阅读

:)