专栏首页前端技匠async/await 源码实现

async/await 源码实现

async/await 是es7带来的新语法糖,可以将内部的异步方法同步处理,看一下下面的例子

async/await 应用场景

如果你有一个这样的场景,b依赖于a,c依赖于b,那么我们只能通过promise then的方式实现。这样的的可读性就会变得很差,而且不利于流程控制,比如我想在某个条件下只走到 b 就不往下执行 c 了,这种时候就变得不是很好控制!

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

async/await实现上述代码,可读性跟流程控制都变的很方便,但是异常捕获只能通过try/catch来实现

async () => {
  try {
    const resA = await Promise.resolve(a);
    // do something
    const resB = await Promise.resolve(b);
    // do something
  } catch(e) {
    console.log(e)
  }
}

生成器(generator)

在想知道 async/await 实现原理之前,我们要首先了解生成器(generator),其实async/await看起来,像极了generator(生成器),只是生成器它不能自动迭代,只能手动触发。举个?

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

let g = gen();

g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.next() // { value: 3, done: true }
  • g.next() 返回的是一个对象 { value: 1, done: false }
  • value 每调一次就会执行下一个 yield,value 就是 yield 的一个值,比如第一次就是 1,第二次就是 2
  • done done 的值为 true 则说明下面已经没有可迭代项了
  • yield 你可以认为是一个 return,会阻断下面代码的执行,并且会将后面的数值返回回去

生成器自迭代

我们可以试想一下,如果生成器能够自动执行所有的迭代任务的话,是否执行下次迭代由 Promise 来决定,那么我们就可以实现 async/await 了?

为什么必须是 Promise?

因为 Promise 用于表示一个异步操作的最终完成 (或失败), 及其结果值。最适合用来判断上一个动作的状态。

如何实现自迭代?

思路:

  1. 通过递归调用生成器对象 next 函数。
function _asyncToGenerator(fn) {
  return function() {
    var self = this,
      args = arguments;
    // 将返回值promise化
    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);
    });
  };
}
  1. 上次 Promise 执行完成后,立即执行下一步,迭代器状态 done = true 时结束
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) {
    // 迭代器完成,将返回值(return)保存起来
    resolve(value);
  } else {
    // -- 这行代码就是精髓 --
    // 将所有值promise化
    // 比如 yield 1
    // const a = Promise.resolve(1) a 是一个 promise
    // const b = Promise.resolve(a) b 是一个 promise
    // 可以做到统一 promise 输出
    // 当 promise 执行完之后再执行下一步
    // 递归调用 next 函数,直到 done == true
    Promise.resolve(value).then(_next, _throw);
  }
}
  1. 接下来测试一下我们写的生成器
const asyncFunc = _asyncToGenerator(function* () {
  console.log(1);
  yield new Promise(resolve => {
    setTimeout(() => {
      resolve();
      console.log('sleep 1s');
    }, 1000);
  });
  console.log(2);
  const a = yield Promise.resolve('a');
  console.log(3);
  const b = yield Promise.resolve('b');
  const c = yield Promise.resolve('c');
  return [a, b, c];
})

asyncFunc().then(res => {
  console.log(res)
});

// 运行结果
// 1
// sleep 1s
// 2
// 3
// ["a", "b", "c"]
  1. 与使用 async/await 对比
const func = async () => {
  console.log(1)
  await new Promise((resolve) => {
    setTimeout(() => {
      resolve()
      console.log('sleep 1s')
    }, 1000)
  })
  console.log(2)
  const a = await Promise.resolve('a')
  console.log(3)
  const b = await Promise.resolve('b')
  const c = await Promise.resolve('c')
  return [a, b, c]
}

func().then(res => {
  console.log(res)
})

// 运行结果
// 1
// sleep 1s
// 2
// 3
// ["a", "b", "c"]

可以看出,我们的代码与 async/await 的输出完全一致,最后再通过 babel等工具 做一些词法转换就可以了

生成器实现

虽然我们已经完成了对 async/await 的实现,但是作为一个好奇猫,我们还想知道 generator 到底怎么实现的?为什么能够阻断我们代码的执行,下次调用的时候再走下一个 yield。这好像很难用 js 代码去解释!

还是使用我们刚才的?,我们看一下 babel 是怎么实现生成器的?

看完这段代码之后,一定会打破你的认知!原来代码还可以这样写!

// 这是我们的异步生成器
var asyncFunc = _asyncToGenerator(
// regeneratorRuntime 这个对象是 迭代器的运行时,mark函数 将所有的变量保存在它作用域下
regeneratorRuntime.mark(function _callee() {
  var a, d, b, c;
  // wrap 是对下面代码片段的一个包裹函数,每执行一次迭代就会调用一次 _callee$
  // _context.next, 执行完本次迭代后将指针指到下一个迭代
  return regeneratorRuntime.wrap(function _callee$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          // --------- ⬇⬇ 这是第一个代码片段 ⬇⬇ -----------
          console.log(1);
          _context.next = 3;
          return new Promise(function (resolve) {
            setTimeout(function () {
              resolve();
              console.log('sleep 1s');
            }, 1000);
          });
          // --------- ⬆⬆ 这是第一个代码片段 ⬆⬆ -----------
        case 3:
          // --------- ⬇⬇ 这是第二个代码片段 ⬇⬇ -----------
          console.log(2);
          _context.next = 9;
          return Promise.resolve('a');
          // --------- ⬆⬆ 这是第二个代码片段 ⬆⬆ -----------
          // ...
          // ... 下面以此类推每一个 yield 会被放进一个 case,作为一个代码片段,
          // ... 每次执行完就return,并且将 _context.next 指向下一个
          // ... 等待下次调用
        case 9:
          d = _context.sent;
          console.log(3);
          _context.next = 13;
          return Promise.resolve('b');

        case 13:
          b = _context.sent;
          _context.next = 16;
          return Promise.resolve('c');

        case 16:
          c = _context.sent;
          return _context.abrupt("return", [a, b, c, d]);

        case 18:
        case "end":
          // 最后执行 stop 结束
          return _context.stop();
      }
    }
  }, _callee);
}));

asyncFunc().then(function (res) {
  console.log(res);
});

如此巧妙的构思,让我对代码有了新的认识。通过词法解析将代码分割成多个片段,用现有的语法实现未来的功能,实在是很巧妙。

体会

通过这次的源码学习,让我打破了原本的思维模式,通过源码更加了解到了 promise 的妙用,以及词法的巧妙转换,都让我感触颇深。

本文分享自微信公众号 - 前端技匠(gh_057b814fc3f7),作者:罗学

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 原生JS灵魂之问(下), 冲刺进阶最后一公里(附个人成长经验分享)

    笔者最近在对原生JS的知识做系统梳理,因为我觉得JS作为前端工程师的根本技术,学再多遍都不为过。打算来做一个系列,一共分三次发,以一系列的问题为驱动,当然也会有...

    用户4131414
  • vue-router的超神之路

    本文是vue-router系列。这里从浏览器到vue-router原理到最佳实践都会有详细的讲解。由于篇幅较长,建议可以选择感兴趣的目录看。

    用户4131414
  • 如何写一个代码编辑器

    学无止境,我们现在的技术都是基于前一代人之上做出来的,要想成为一个高级/资深的前端,重点不是你能创造多少东西,而是你知道多少东西。有没有解决问题的能力。遇到问题...

    用户4131414
  • Tensorflow入门教程(二十七)——多尺度深监督VNet(MSDSVNet)

    上两篇我分享了深度监督VNet和多尺度VNet,效果已经有所改善,进一步改进的话,非常简单的想法就是把两者进行结合,那么今天我将分享多尺度深度监督VNet。

    用户7498388
  • 自定义输入框造轮子大法ZHAddressTextFiled

    这个输入框是输入把默认提示语上移,我记得有第三方库,但是我就想造轮子,所以就除了这个。

    君赏
  • 2019中国夜间经济发展报告:​文化需求成蓝海 | 每周文旅资讯精选(11.18-11.24)

    ? ? 中国首个滑雪场联盟成立 11月18日,由万龙度假天堂、北大湖滑雪度假区、鳌山滑雪场、丝绸之路国际度假区、亚布力阳光度假村共5个滑雪场组成的滑雪场联盟正...

    腾讯文旅
  • 利用 Canvas API 实现正方验证码的识别与自动填充

    新学期伊始,年度抢课大戏同步上映,学校的正方教务系统也迎来了前所未有的流量冲击。教务系统这个跑在 Windows 2003 的上古时期的 ASP.NET 程序的...

    zgq354
  • 看懂云计算、虚拟化和容器,这一篇就够啦!

    作为信息科技发展的主流趋势,它频繁地出现在我们的眼前。伴随它一起出现的,还有这些概念名词——OpenStack、Hypervisor、KVM、Docker、K8...

    鲜枣课堂
  • 剑指offer 把二叉树打印成多行

    week
  • SpringBoot配置文件的加载位置

    用户1112962

扫码关注云+社区

领取腾讯云代金券