手写async、await、generator核心逻辑

5,727 阅读4分钟

前言

群里聊天有人说面试问到了 async 的实现原理,并且回答中提到了 generator,最近正好学习了一下标题中的三个玩意,觉得回答上有点不妥,产生了一番折腾,最后自己手写了这三个东西的大致逻辑。

本文没有详细源码的解析,属于个人学习过程中的理解方式,如果想学习可以参考文章中给出的链接,如果有任何意见/建议欢迎指出。

async

以下是群友回答。问题是 async 函数的实现原理,在单论 async 时,我觉得与 generator 并没有关系。

我们来看一下 MDN 上的描述:

async function 用来定义一个返回 AsyncFunction 对象的异步函数。异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的 Promise 返回其结果。

在看一下 MDN 上的转换结果:

For example, the following:

async function foo() {
   return 1
}

is equivalent to:

function foo() {
   return Promise.resolve(1)
}

所以单论 async 的实现,它更应该类似于下面的代码:

function _async(fn) {
    return (...args) => Promise.resolve(fn(...args));
}

generator

再来看看 generator 是什么。先实现 generator 是为了在接下来的 await 中使用。以下是廖雪峰教程中的示例代码:

function* fib(max) {
    var
        t,
        a = 0,
        b = 1,
        n = 0;
    while (n < max) {
        yield a;
        [a, b] = [b, a + b];
        n ++;
    }
    return;
}

var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}

可以理解为调用 generator 返回的“对象”上的 next 方法,可以产生类似于 { value, done } 形式的结果。

再来看一下掘友写代码像蔡徐抻9k字 | Promise/async/Generator实现原理解析这篇文章里写的 generator 转换结果:

// 代码
function* foo() {
  yield 'result1'
  yield 'result2'
  yield 'result3'
}
  
const gen = foo()
console.log(gen.next().value)
console.log(gen.next().value)
console.log(gen.next().value)

// babel官网转换结果
"use strict";

var _marked =
/*#__PURE__*/
regeneratorRuntime.mark(foo);

function foo() {
  return regeneratorRuntime.wrap(function foo$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 'result1';

        case 2:
          _context.next = 4;
          return 'result2';

        case 4:
          _context.next = 6;
          return 'result3';

        case 6:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

var gen = foo();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);

在群里探讨的过程中,为了区别 asyncawait,写了如下的一段“generator”:

function get() {
  let g = {
    done: false,
    count: 0,
    next() {
      if (this.count === 3) this.done = true;
      if (this.done) return { value: this.count, done: this.done };
      this.count++;
      return { value: this.count, done: this.done };
    }
  }
  return g;
}

let obj = get();
console.log(obj.next())
console.log(obj.next())
console.log(obj.next())
console.log(obj.next())
console.log(obj.next())

// 输出结果
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 3, done: true }
// { value: 3, done: true }

以上代码与 babel 以及 ES 中的实现肯定差别很大,但是基本上就是一个 generator 的执行逻辑。具体可以参考 babel 转码后的结果中的 switch (_context.prev = _context.next) 以及几行 _context.next = 2; 代码。

await

先回顾一下 await的作用,它能够将异步代码转变成同步代码的逻辑,在异步代码返回值到达前,程序将会被挂起。所以我们要挑一个能让程序挂起的代码,也就是 while(true)。实现如下:

function _await() {
    let result = data.next();
    while (true) {
      console.log('waiting...', result); 
      if (result.done) return result.value;
      result = data.next();
    }
}

let g = get();
console.log('before');
let a = _await(g);
console.log(a);
console.log('after');

// 输出
// before
// awaiting... { value: 1, done: false }
// awaiting... { value: 2, done: false }
// awaiting... { value: 3, done: false }
// awaiting... { value: 3, done: true }
// 3
// after

更进一步

在实际使用过程中,await 只能在 async 中使用,群友问这个要怎么实现,于是折腾出下面这一版。

function myAwait() {
  this.c = function(data) {
    if (!this.isCalledByAsync) throw new Error('Should be called by async');
    let result = data.next();
    while (true) {
      console.log('awaiting...', result); 
      if (result.done) return result.value;
      result = data.next();
    }
  }
}

function myAsync(fn) {
  myAwait.prototype.isCalledByAsync = true;
  let m = get();
  console.log('async before');
  let d = new myAwait().c(m);
  console.log(d);
  console.log('async after');
  myAwait.prototype.isCalledByAsync = false;
}
myAsync();

let g = get();
console.log('no async before');
let a = new myAwait().c(g);
console.log(a);
console.log('no async after');

// 输出
// async before
// awaiting... { value: 1, done: false }
// awaiting... { value: 2, done: false }
// awaiting... { value: 3, done: false }
// awaiting... { value: 3, done: true }
// 3
// async after
// no async before
// Error: Should be called by async

没想到其他什么办法来做这个标记,只能使用控制原型的方式来控制是否能够执行,但也算只能在 async 内部才能使用了。

结语

本文是个人学习过程中的理解,如果有理解有误的地方,欢迎各位大佬指教。 本文写出来也是为了各位学习代码的过程中增加一些不同的角度。