手撕 generator 和 async、await

72 阅读5分钟

协程

传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"(coroutine),意思是多个线程互相协作,完成异步任务。

协程有点像函数,又有点像线程。它的运行流程大致如下。

  1. 协程A开始执行。
  2. 协程A执行到一半,进入暂停,执行权转移到协程B。
  3. 一段时间后)协程B交还执行权。
  4. 协程A恢复执行。

Generator 函数

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行),它也是我们实现 async、await 的关键。

function* read(){
  yield 1;
  yield 2;
  yield 3;
  return 'end';
  yield 4;
}

let it = read();  // Object [Generator] {}

console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: 'end', done: true }
console.log(it.next()); // { value: undefined, done: true }

可以看到,Generator 返回的是一个迭代器对象,而且如果遇到 return,则会把 done 置为 true,迭代器的概念不熟悉的同学可以参考我另一篇文章剖析迭代器

yield 的返回值

修改代码,猜猜 a,b,c分包是什么

function* read(){
  const a = yield 1;
  console.log(`a: ${ a }`);
  const b = yield 2;
  console.log(`b: ${ b }`);
  const c = yield 3;
  console.log(`c: ${ c }`);
}

let it = read();

it.next('这里不会赋值');
it.next('这里赋值给 a');
it.next('这里赋值给 b');
it.next('这里赋值给 c');

这个特性的话,我们牢记住一点,第一次 next 不会赋值,之后的每一次 next 方法都会给前一个 yield 的返回值赋值。

解析 Generator 内部实现原理

为了触碰真相,我们通过 bebel编译器 来看下这段代码转成 es5 长什么样子~

整段代码变成了 switch case 形式~

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

function read() {
  var a, b, c; // 声明接收 yield 返回值的变量
  return regeneratorRuntime.wrap(function read$(_context) {
    // while(1) 没实质意义,毕竟每次都 return 了,只是标识该方法不止执行一次
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2; // 指针下移
          return 1; // 返回第一次 yeild 的值,但是没有给 a 赋值哦

        case 2:
          // 第二次调用 next 方法,会把 next 的参数赋值给 上一次的 a
          a = _context.sent; 
          console.log("a: ".concat(a));
          _context.next = 6; // 指针下移
          return 2; // 返回第二次 yield 的值

        case 6:
          // 第二次调用 next 方法,会把 next 的参数赋值给 上一次的 b
          b = _context.sent;
          console.log("b: ".concat(b));
          _context.next = 10;
          return 3;

        case 10:
          // 第三次调用 next 方法,会把 next 的参数赋值给 上一次的 c
          c = _context.sent;
          console.log("c: ".concat(c));

        case 12:
        case "end":
          // 调用 _context 的 stop 方法,把 done 置为 true
          return _context.stop();
      }
    }
  }, _marked);
}

let it = read();

我们来实现 regeneratorRuntime,让这段代码跑起来~

let regeneratorRuntime = {
  // 无实际意义,我们暂且这么实现
  mark(generatorFn) { 
    return generatorFn;
  },
  wrap(iteratorFn) {
    const context = {
      next: 0, // 起始指针
      done: false,
      stop() {
        context.done = true;
      },
      sent: null // 保存的 next 的参数值
    }

    let it = {};

    it.next = function(param) { // 此 value 会传递给上一次 yield 的返回值
      context.sent = param;
      let value = iteratorFn(context);

      return {
        value,
        done: context.done
      }
    }
    
    // 返回一个迭代器对象
    return it;
  }
}

所以,其实 generator 的函数,是由一个 iterator 生成器函数 wrap 包裹着一个 switch case 的迭代函数组合而成的,而且上一次 yield 的返回值是在下一次 next 参数传入并挂载到 context.sent 之后赋予的。

Generator 实现异步串行

平时开发中,我们基本上使用不到 Generator 函数。

思考以下场景:

// 比如我读取两个文件,其中 a.txt 的内容是 b.txt 的地址,这样就出现了依赖关系,必须等 a.txt 
// 结果返回,才能去拿 b.txt
let fs = require('fs').promises;

function* read() {
	const a = yield fs.readFile('a.txt', 'utf8');
	const b = yield fs.readFile(a, 'utf8');
	console.log(b); 
}

let it = read();

// value 是个 promise 对象
let { value, done } = it.next();

value.then(data => {
	// 第二次 next 调用传参是给第一个 yield 的返回值赋值 也就是 a = data
	// 然后继续往下执行	
	let { value, done } = it.next(data);

	value.then(data => {
		// b = data;
		let { value, done } = it.next(data);
	});
});


// 输出 b.txt 的内容

这样写的话,实在是太痛苦了,我们来引用一个叫 co 的库让我们的代码写起来更像同步的。

co 结合 Generator 实现异步串行

基于 Generator 的 node.js 和浏览器的异步解决方案,接收一个 iterator 对象,返回 promise

let fs = require('fs').promises;
let co = require('co');

function* read() {
	const a = yield fs.readFile('a.txt', 'utf8');
	const b = yield fs.readFile(a, 'utf8');
		
	return b;
}

co(read()).then(data => {
  console.log(data);
});

确实优雅了许多,那么它是如何实现的呢,我们来实现下它~

手写 co

// 前置知识点
//   @1 同步迭代用 for 循环,比如 Promise.all 我们是希望并发执行,而异步跌代我们使用递归

function _co(it) {
  return new Promise((resovle, reject) => {
    function next(data) {
      let { value, done } = it.next(data);

      if (done) {
        // 递归终止条件 返回最终结果
        return resovle(value);
      }
      
      // 兼容 value 不是 promise 对象的情况
      // 浏览器内部 resolve 方法会判断如果参数是个 promise,
      // 直接把 promise返回,不用担心性能问题
      Promise.resolve(value).then(data => {
        next(data); // 递归 把参数传入
      }, reject); // 执行失败直接 reject
    }

    next();
  });
}

async + await的实现「co + generator 的语法糖」

我们来看下更高级的方式,异步解决的终极方案

let fs = require('fs').promises;

async function read() {
	try {
		const a = await fs.readFile('a.txt', 'utf8');
		const b = await fs.readFile(a, 'utf8');

		return b;
	} catch(e) {
		console.log(e);
	}
}

read().then(data => {
	console.log('success', data); // success  b.txt文件内容
}, err => {
	console.log(err);
})

我们继续之前的做法,去 bebel 官网看下 async、await 的实现方式:

"use strict";

// 其实就是 co 的辅助方法
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
    try {
        var info = gen[key](arg); // var info = it.next(value)
        var value = info.value; // 拿到此次 next 返回的 value
    } catch (error) {
        reject(error); // 如果报错,直接返回错误
        return;
    }
    if (info.done) {
        // 如果完成,把值抛出去
        resolve(value);
    } else {
        // 没完成,继续
        Promise.resolve(value).then(_next, _throw);
    }
}

// 对比 generator 实现方式,新增的方法
// 仔细看,这不就是我们上面刚刚实现的 co 么
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 read() {
    return _read.apply(this, arguments);
}

function _read() {
    _read = _asyncToGenerator( /*#__PURE__*/ regeneratorRuntime.mark(function _callee() {
        var a, b;
        // 明显的 generator 方法
        return regeneratorRuntime.wrap(function _callee$(_context) {
            while (1) {
                switch (_context.prev = _context.next) {
                    case 0:
                        _context.prev = 0;
                        _context.next = 3;
                        return fs.readFile('a.txt', 'utf8');

                    case 3:
                        a = _context.sent;
                        _context.next = 6;
                        return fs.readFile(a, 'utf8');

                    case 6:
                        b = _context.sent;
                        return _context.abrupt("return", b);

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

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

可以看出,async + await 最后被编译成了 co + generator,所以其实 async 和 await 只是 co + generator 的语法糖。