【若川视野 x 源码共读】第4期 | co 源码 | 从co看js异步编程

820 阅读5分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

co模块简介

TJ Holowaychukejs等数百个开源项目的作者) 于 2013 年 6 月发布的一个小工具co 模块,用于 Generator 函数的自动执行.

image.png

Generator based control flow goodness for nodejs and the browser, using promises, letting you write non-blocking code in a nice-ish way.

前置知识

js异步编程发展史

  1. callback: 强耦合,回调地狱(嵌套的回调)

  2. promise: ES6(ES2015) then+回调函数 (链式回调)

  3. generator: ES6(ES2015) 需要手动执行next

  4. 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 原生支持协程,另一方面又会导致两个问题:

  1. 不能自动执行generator中的yield

  2. 不能将上个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用法举例

官网例子

使用规范

代码流程图

image.png 核心思想

调用gen.next(onFulfilled())得到ret,将ret.value转成promise,在then方法中调用gen.next(onFulfilled(ret.value)),直到ret.done为true。

对比丐版co,co模块还做了什么?

  1. 将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) {

// ...

});

}

  1. 将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) + '"'));

}

  1. 错误处理

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);

}

  1. 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 }

其他一些有趣的点

  1. 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);
      });
    });
  }
  1. 常用类型判断

主要是通过对象独有的特征做判断。


/**

* 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;

}

  1. 通过按位取反~判断字符串中是否包含指定字符

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有哪些异同呢?

比较项coasync 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

总结

  1. 回顾异步编程发展史,js异步编程从callback到async await,向着更加语义化,对开发者友好的方向发展。

  2. 通过对co源码解读,了解怎样实现Generator的自执行。

  3. 通过对比co和async await,了解二者的联系与区别。

参考文献

  1. Thunk 函数的含义和用法

  2. co 函数库的含义和用法

  3. Generator 函数的语法

  4. Generator 函数的异步应用