本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
co模块简介
TJ Holowaychuk(ejs等数百个开源项目的作者) 于 2013 年 6 月发布的一个小工具co 模块,用于 Generator 函数的自动执行.
Generator based control flow goodness for nodejs and the browser, using promises, letting you write non-blocking code in a nice-ish way.
前置知识
js异步编程发展史
-
callback: 强耦合,回调地狱(嵌套的回调)
-
promise: ES6(ES2015) then+回调函数 (链式回调)
-
generator: ES6(ES2015) 需要手动执行next
-
async await: ES2017 近似同步代码的书写体验
generator 用法回顾
function* gen(x) {
const y = yield x + 2;
return y;
}
const g = gen(1); // 返回一个遍历器对象
g.next(); // {value: 3, done: false}
g.next(); // {value: undefined, done: false}
generator 的特性
可以暂停执行和恢复执行
这个特性一方面使得generator 原生支持协程,另一方面又会导致两个问题:
-
不能自动执行generator中的yield
-
不能将上个yield的值传入下个next
怎样解决generator的自执行问题。
在看co源码之前,可以自行写一个丐版的co,解决例子中generator自执行问题。
function simplifiedCo(genFun) {
const args = Array.prototype.slice.call(arguments, 1);
const g = genFun(...args);
let res;
res = g.next();
while (!res.done) {
res = g.next(res.value);
}
return res.value;
}
核心思想
调用gen.next 直到 res.done 为 true;
局限性:
没有考虑yield后面是generator、promise等情况。
co用法举例
代码流程图
核心思想:
调用gen.next(onFulfilled())得到ret,将ret.value转成promise,在then方法中调用gen.next(onFulfilled(ret.value)),直到ret.done为true。
对比丐版co,co模块还做了什么?
- 将gen的返回值包装成promise
源码中注释如下:
we wrap everything in a promise to avoid promise chaining, which leads to memory leak errors. see github.com/tj/co/issue…
这里不是很理解,为何包装成promise可以解决内存泄漏问题。
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
// ...
});
}
- 将ret.value转 promise
将generator、promise、function,array,object等不同对象统一处理成Promise。
/**
* Convert a `yield`ed value into a promise.
*
* @param {Mixed} obj
* @return {Promise}
* @api private
*/
function toPromise(obj) {
if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
然后就可以方便地在then方法的onFulfilled和 onRejected将执行权交给generator的遍历器对象。
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
- 错误处理
onFulfilled 和 onRejected 中都有错误捕获,并调用最终返回的promise的reject方法。
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
- co.wrap
一个高阶函数,可以将Generator函数转成可以自执行的函数
/**
* Wrap the given generator `fn` into a
* function that returns a promise.
* This is a separate function so that
* every `co()` call doesn't create a new,
* unnecessary closure.
*
* @param {GeneratorFunction} fn
* @return {Function}
* @api public
*/
co.wrap = function (fn) {
// 自定义属性
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
使用方法:
const {
wrap, co
} = require('../index');
function* gen(x) {
const y = yield {res:x+2};
return y;
}
const onfulfilled = console.log;
co(gen,1).then(onfulfilled); // { res: 3 }
co(gen,2).then(onfulfilled); // { res: 4 }
const wrappedGen = wrap(gen); // wrap 函数将generator函数封装成 可以自执行的函数
wrappedGen(1).then(onfulfilled); // { res: 3 }
wrappedGen(2).then(onfulfilled); // { res: 4 }
其他一些有趣的点
- thunk函数
入参为回调函数的函数。co中的thunk函数接收err和res两个参数,分别代表thunk函数抛出的错误和执行结果。
co会将thunk函数转成promise处理。
function thunkToPromise(fn) {
var ctx = this;
return new Promise(function (resolve, reject) {
fn.call(ctx, function (err, res) {
if (err) return reject(err);
console.log(arguments,'arguments');
if (arguments.length > 2) res = slice.call(arguments, 1);
resolve(res);
});
});
}
- 常用类型判断
主要是通过对象独有的特征做判断。
/**
* Check if `obj` is a promise.
*
* @param {Object} obj
* @return {Boolean}
* @api private
*/
function isPromise(obj) {
return 'function' == typeof obj.then;
}
/**
* Check if `obj` is a generator.
*
* @param {Mixed} obj
* @return {Boolean}
* @api private
*/
function isGenerator(obj) {
return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}
/**
* Check if `obj` is a generator function.
*
* @param {Mixed} obj
* @return {Boolean}
* @api private
*/
function isGeneratorFunction(obj) {
var constructor = obj.constructor;
if (!constructor) return false;
if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
return isGenerator(constructor.prototype);
}
/**
* Check for plain object.
*
* @param {Mixed} val
* @return {Boolean}
* @api private
*/
function isObject(val) {
return Object == val.constructor;
}
- 通过按位取反~判断字符串中是否包含指定字符
const str = 'There is a cat';
~str.indexOf('cat'); // -12
~str.indexOf('dog'); // 0
根据按位取反~index的结果为0或非0数字
,判断是否包含指定字符。
co vs async await
co模块诞生于2013年,js异步编程书写格式同步化的过程中。它是gennerator向async await发展的过渡性产物。它与后来出现的async await有哪些异同呢?
比较项 | co | async await |
---|---|---|
本质 | Generator 的执行器 | Generator 函数的语法糖(内置执行器) |
适用范围 | co模块执行的Generator函数yield命令后面只能是 promise、thunk函数 (functions)、array (并行执行)、objects (并行执行)、generators (delegation)、generator functions (delegation) | async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象) |
返回值 | Promise对象 | Promise对象 |
语义化 | 略差co(gen)不能直接看出作用 | 较好,async 代表函数中有异步操作,await表示该表达式需要等待异步结果 |
对比表格,可以看出 co是对generator功能的补充,是从generator 向async await发展的中间产物,async await约等于co+generator+promise
总结
-
回顾异步编程发展史,js异步编程从callback到async await,向着更加语义化,对开发者友好的方向发展。
-
通过对co源码解读,了解怎样实现Generator的自执行。
-
通过对比co和async await,了解二者的联系与区别。