协程
传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"(coroutine),意思是多个线程互相协作,完成异步任务。
协程有点像函数,又有点像线程。它的运行流程大致如下。
- 协程A开始执行。
- 协程A执行到一半,进入暂停,执行权转移到协程B。
- 一段时间后)协程B交还执行权。
- 协程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 的语法糖。