穿越时空的等待——async/await解密(三)

727 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

在《塞尔达传说·旷野之息》中,如果遇到较强的对手我们往往应该优先考虑服用三倍攻击料理。来吧,也尝尝啸达同学新鲜出炉的async/await三攻料理!!!

观看指南

前言

上一篇中我们介绍了async...await...中的神奇魔法 —— 协程。文中最后的地方我提到了业界关于Nodejs是否支持协程这件事展开了激烈的讨论。我就用其中的一则示例展开我们今天的学习。

async function foo() {
    await bar();
    return 42;
}

async function bar() {
    await Promise.resolve();
    throw new Error('Error');
}

foo().catch(error => {
    console.log(error.stack);
});

先看一下上述代码,这段代码没什么特殊意义,就是想打印一下异常的调用栈。我当前使用的开发电脑上node的版本是v16.11.1。我先尝试通过自己的机器运行一下这段代码:

image_1649476716086.png

紧接着,我有一台虚机,之前为了调测装了一个v8.10.0版本的NodeJS。我们用这台机器执行一下同一段代码:

image_1649476899749.png

首先这里明确两点:1. 异常肯定是bar中抛出的;2. 两个版本最后展现的形式虽然不同,但是try...catch...最终都成功地捕获到了异常。

当在16+版本环境执行的时候,异常堆栈中出现了 foo的影子;而在8+版本中,异常堆栈中没有foo的调用信息。这就让我们明显感觉到,不同版本NodeJS对async...await...的支持是不一样的。上一篇讲的协程是ES6之后的产物,自然对应的是高版本里才有的东西。那低版本,也就是ES5中是怎么处理async...await...的呢?

导航3

code_1649522616150.png

  • 为什么try...catch...不能捕获异步异常
  • 简介Promise解决方案
  • 进程、线程、协程,傻傻分不清楚
  • show me your code 👈👈👈

测试代码

废话不多说,先上原材料。测试代码比较简单,主要目的是要测试async...await...的,同时在异步逻辑中尝试抛出异常,看看try...catch...是怎么捕获到异常的。

async function test() {
  try {
    const result1 = await "result1";
    const result2 = await timer(true);
    const result3 = await timer(false);
    console.log(result1, result2, result3);
  } catch (e) {
    console.log("err: ", e);
  }
}

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

test().then((res) => console.log(res));

babel一下

接下来,我通过Babel工具将代码转换成ES5代码:

"use strict";

// 这里其实是个polyfill,我自己加进去的,跟Babel没关系
const regeneratorRuntime = require("./regenerator-runtime/runtime")

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

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

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) {
          while (1) {
            switch ((_context.prev = _context.next)) {
              case 0:
                _context.prev = 0;
                _context.next = 3;
                return "result1";

              case 3:
                result1 = _context.sent;
                _context.next = 6;
                return timer(true);

              case 6:
                result2 = _context.sent;
                _context.next = 9;
                return timer(false);

              case 9:
                result3 = _context.sent;
                console.log(result1, result2, result3);
                _context.next = 16;
                break;

              case 13:
                _context.prev = 13;
                _context.t0 = _context["catch"](0);
                console.log("err: ", _context.t0);

              case 16:
              case "end":
                return _context.stop();
            }
          }
        },
        _callee,
        null,
        [[0, 13]]
      );
    })
  );
  return _test.apply(this, arguments);
}

var timer = function timer(success) {
  // 模拟一个简单的异步操作
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      if (success) {
        resolve("success");
      }

      reject("fail");
    }, 1000);
  });
};

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

代码有点多,各位老铁先别慌,我们一点一点分析

_callee$()方法

我们先看看_test()里面都干了啥,把这里面的东西吃透了,代码就看明白了。我们从内往外扒,最内层函数为_callee$。从内容上看,这块其实就是一个状态表。很多人应该做过性格测试,这个状态表就类似那种性格测试:

题目1:选出你第一时间看到的颜色
A. 红色 —— 请转到第3题
B. 蓝色 —— 请转到第5题
C. 绿色 —— 请转到第7题
D. 啥也没看到 —— 眼瞎,鉴定完毕!

function _callee$(_context) {
  while (1) {
    switch ((_context.prev = _context.next)) {
      case 0:
        _context.prev = 0;
        _context.next = 3;
        return "result1";

      case 3:
        result1 = _context.sent;
        _context.next = 6;
        return timer(true);

      case 6:
        result2 = _context.sent;
        _context.next = 9;
        return timer(false);

      case 9:
        result3 = _context.sent;
        console.log(result1, result2, result3);
        _context.next = 16;
        break;

      case 13:
        _context.prev = 13;
        _context.t0 = _context["catch"](0);
        console.log("err: ", _context.t0);

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

regeneratorRuntime.wrap

接下看下wrap函数,wrap函数相当于给“性格测试”外面做了一层包装。

regeneratorRuntime.wrap(
  function _callee$(_context) {
    ...
  },
  _callee,
  null,
  [[0, 13]]
);

首先介绍一下regeneratorRuntime模块,regeneratorRuntime模块来自facebook的regenerator模块,源码在这里。其作用就是给Generator做语法兼容的,你可以把它理解为Generator的Polyfill。这个代码有大几百行,这里不会把每一行都解释到,只会分析部分核心代码。

function wrap(innerFn, outerFn, self, tryLocsList) {
    // 此处有省略...
    var context = new Context(tryLocsList || []);  
  
    // makeInvokeMethod(innerFn,self,context)函数  
    // 参数innerFn待控制执行的函数,self为innerFn执行时的上下文,context为控制innerFn条件分支执行状态的操纵对象  
    // 返回invoke函数,触发innerFn函数以特定的条件分支执行,或报错,或终结生成函数  
    generator._invoke = makeInvokeMethod(innerFn, self, context);  
  
    return generator;  
  } 

wrap函数里最主要的动作就是绑定了makeInvokeMethod函数,makeInvokeMethod函数负责根据状态表进行状态之间切换的。就像那个性格测试一样,当你选择红色时,makeInvokeMethod会记录你当前的选择;从状态表中检查如果你选了红色,那下一题应该是哪一题?同时引导你开始答下一题:

// innerFn就是上文的_callee$,就是那个“性格测试”
function makeInvokeMethod(innerFn, self, context) {
  // 设置开始状态
  var state = GenStateSuspendedStart;

  // method是当前需要执行的动作:throw | return | next
  return function invoke(method, arg) {
    // 此处有省略...
    context.method = method;
    context.arg = arg;

    while (true) {
      // 此处有省略...

      if (context.method === "next") {
        // Setting context._sent for legacy support of Babel's
        // function.sent implementation.
        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;

      // innerFn是状态表函数,通过执行状态函数找出当前应该下一个状态,同时返回当前状态的结果
      var record = tryCatch(innerFn, self, context);
      if (record.type === "normal") {
         // 此处有省略...
        return {
          value: record.arg,
          done: context.done
        };

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

regeneratorRuntime.mark

regeneratorRuntime.mark会把_callee的prototype设置为Generator。Generator.prototype有三个方法,分别是nextreturnthrow。这三个方法的执行逻辑会在regeneratorRuntime.wrap中实现。

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

_asyncToGenerator && asyncGeneratorStep

如果研究过tj大神的co模块的,应该对下面代码有似曾相识的感觉。_asyncToGeneratorco十分类似,也是一个Generator的自动执行器。_asyncToGenerator中先通过执行_next触发了自动执行机。当状态机返回结果后,asyncGeneratorStep会将结果包装成Promise,同时将_next_throw当作这个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);
  }
}

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

异常是怎么捕获到的

其实从编译后的结果我们不难发现,我们在源代码中的try...catch...被编译成一个奇怪的数组[[0, 13]]。这个数组就代表源代码try...catch...的作用域。[[0, 13]]就代表case0 ~ 13之间的异常都需要被try...catch...进行处理。

image_1649520115332.png

这里让我们简单跟着代码逻辑捋一下:

  1. 当执行到case9时,这时返回一个timer(false)的Promise,同时将next指针指向9
  2. asyncGeneratorStep在处理这个Promise时,发现Promise的状态被改为Rejected,于是执行_throw方法
  3. makeInvokeMethod方法执行throw逻辑时会检查[[0, 13]]数组,发现prev=6是处于[0, 13]这个范围之内的,则修改next为13
  4. 状态机转去执行13的逻辑,打印异常。

图例

我把测试代码的整个运行逻辑结合代码画了一下,我相信借助这个图可以更好地帮大家理解整个运行过程,从而更好的理解源码。

image_1649521040235.png

总结

经过一番分析不难发现,async...await...在ES5中是通过Promise实现的。Promise对异步的处理我在跨越时空的等待之一中已经讲过了。到此,三篇的内容就此形成一个闭环,我自认为已经尽自己最大的努力把async...await...中异步异常的捕获的内容讲清楚了。

还记得在第一篇中介绍背景知识的时候,我把主函数和异步逻辑比作平行时空下的你和ta,感谢async...await...将宇宙打通,让我们有了跨越时空的能力、感知彼此的动力。所以async...await...承载的不仅仅是对异步的等待,某种程度上也给人以美好的寄托,告诉我们有些人值得等待,有些美好终将到来!