前言

原文地址

源码地址

了解co的前提是已经知晓generator是什么,可以看软大神的Generator 函数的语法,
co是TJ大神写的能够使generator自动执行的函数库,而我们熟知的koa也用到了它管理异步流程控制,将异步任务书写同步化,爽的飞起,也摆脱了一直以来的回调地狱问题。

如何使用

首先我们根据co的官方文档来稍做改变看下,到底如何使用co,再一步步进行源码分析工作(这篇文章分析的co版本是4.6.0)。

yield 后面常见的可以跟的类型

  1. promises

  2. array (parallel execution)

  3. objects (parallel execution)

  4. generator functions (delegation)

promises

let co = require('co')
let genTimeoutFun = (delay) => {
return () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`delayTime: ${delay}`)
}, delay)
})
}
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200) co(function * () {
let a = yield timeout1()
console.log(a) // delayTime: 1000
let b = yield timeout2()
console.log(b) // delayTime: 200 return 'end'
}).then((res) => {
console.log(res)
})

array

let co = require('co')
let genTimeoutFun = (delay) => {
return () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`delayTime: ${delay}`)
}, delay)
})
}
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200) co(function * () {
let a = yield [timeout1(), timeout2()]
console.log(a) // [ 'delayTime: 1000', 'delayTime: 200' ]
return 'end'
}).then((res) => {
console.log(res) // end
})

objects

let co = require('co')
let genTimeoutFun = (delay) => {
return () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`delayTime: ${delay}`)
}, delay)
})
}
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200) co(function * () {
let a = yield {
timeout1: timeout1(),
timeout2: timeout2()
}
console.log(a) // { timeout1: 'delayTime: 1000',timeout2: 'delayTime: 200' }
return 'end'
}).then((res) => {
console.log(res) // end
})

generator functions

let co = require('co')
let genTimeoutFun = (delay) => {
return () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`delayTime: ${delay}`)
}, delay)
})
}
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200) function * gen () {
let a = yield timeout1()
console.log(a) // delayTime: 1000
let b = yield timeout2()
console.log(b) // delayTime: 200
} co(function * () {
yield gen() return 'end'
}).then((res) => {
console.log(res) // end
})

最后说一下,关于执行传入的generator函数接收参数的问题

let co = require('co')

co(function * (name) {
console.log(name) // qianlongo
}, 'qianlongo')

从co函数的第二个参数开始,便是传入的generator函数可以接收的实参

开始分析源码

你可以把以上代码拷贝至本地测试一番看看效果,接下来我们一步步开始分析co的源码

首先经过上面的例子可以发现,co函数本身接收一个generator函数,并且co执行后返回的是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) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen); // xxx
});
}

在Promise的内部,先执行了外部传入的gen,执行的结果如果不具备next属性(且要是一个函数),就直接返回,并将执行成功回调resolve(gen),否则得到的是一个指针对象。

接下来继续看...


onFulfilled(); /**
* @param {Mixed} res
* @return {Promise}
* @api private
*/ function onFulfilled(res) {
var ret;
try {
ret = gen.next(res); // 用上面执行gen之后的generator生成器将指针指向下一个位置
} catch (e) {
return reject(e);
}
next(ret); // 紧接着执行next,正是它实现了反复调用自己,自动流程控制,注意ret(即上一次gen.next执行后返回的对象{value: xxx, done: true or false})
} /**
* @param {Error} err
* @return {Promise}
* @api private
*/ function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}

我觉得可以把 onFulfilledonRejected 看成是返回的Promise的resolvereject

onFulfilled也是将原生的generator生成器的next方法包装了一遍,大概是为了抓取错误吧(看到内部的try catch了吗)

好,我们看到了co内部将指针移动到了第一个位置之后,接着执行了内部的next方法,接下来聚焦在该函数上


function next(ret) {
// 如果整个generator函数的内部状态已经表示走完,便将Promise的状态设置为成功状态,并执行resolve
if (ret.done) return resolve(ret.value);
// 这一步是将ret的value转换为Promise形式
var value = toPromise.call(ctx, ret.value);
// 这里非常关键,是co实现自己调用自己,实现流程自动化的关键
// 注意这里使用value.then,即为返回值添加成功和失败的回调,在成功的回调里面再去执行onFulfilled,紧接着就是调用内部的next函数
// 那不是就保证了流程完全按照你写的顺序来了?
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// 抛出错误,yield后只能跟着指定的下列这几种类型
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}

聪明的你,是不是已经明白了co是怎么将异步流程自动管理起来了

但是我对next函数中的toPromise函数还有疑问,他到底做了什么事?使得co(generatorFun)中yield可以支持数组、对象、generator函数等形式。

一步步来看


function toPromise(obj) {
// obj不存在,直接返回
if (!obj) return obj;
// 如果obj已经是Promise,则也是直接返回
if (isPromise(obj)) return obj;
// 如果是个generator函数或者generator生成器,那就像你自己调用co函数一样,手动传到co里面去执行
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
// 如果obj既不是Promise,也不是isGeneratorFunction和isGenerator,要是一个普通的函数(需要符合thunk函数规范),就将该函数包装成Promise的形式
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
// 如果是一个数组的形式,就去arrayToPromise包装一番
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}

首先如果obj不存在,就直接返回,你想啊,co本来就是依赖上一次指针返回的value是Promise或者其他,这个时候如果返回

{
value: false,
done: false
}

那就没有必要再给一个false值转成Promise形式了吧。

接着,如果obj本身就是个Promise也是直接返回,用了内部的isPromise函数进行判断,我们看下他怎么实现的。


function isPromise(obj) {
return 'function' == typeof obj.then;
}

其实就是判断了obj的then属性是不是个函数

再接着,如果是个generator函数或者generator生成器,那就像你自己调用co函数一样,手动传到co里面去执行。

isGeneratorFunction


function isGeneratorFunction(obj) {
var constructor = obj.constructor;
if (!constructor) return false;
if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
return isGenerator(constructor.prototype);
}

通过obj的constructor属性去判断其是否属于GeneratorFunction,最后如果constructor属性没判断出来,再去用isGenerator,判断obj的原型是不是generator生成器

function isGenerator(obj) {
return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}

判断的条件也比较直接,需要符合两个条件,一个是obj.next要是一个函数,一个是obj.throw要是一个函数

接下来继续看

如果obj既不是Promise,也不是isGeneratorFunction和isGenerator,要是一个普通的函数,就将该函数包装成Promise的形式,这里我们主要需要看thunkToPromise

function thunkToPromise(fn) {
var ctx = this;
// 将thunk函数包装成Promise
return new Promise(function (resolve, reject) {
// 执行这个thunk函数
fn.call(ctx, function (err, res) {
// 注意thunk函数内部接收的回调函数中传入的第一个参数是err,出现了err,当然需要走reject了
if (err) return reject(err);
// 参数是两个以上的情况下,将参数整成一个数组
if (arguments.length > 2) res = slice.call(arguments, 1);
// 最后执行成功的回调
resolve(res);
});
});
}

接下来是重头戏了,co中如果处理yield后面跟一个数组呢?主要是arrayToPromise函数的作用

function arrayToPromise(obj) {
// 使用到了Promise.all,将obj中多个promise实例(当然你也可以在数组中填thunk函数,generator函数等)重新包装成一个。最后返回一个新的Promise
return Promise.all(obj.map(toPromise, this));
}

还有最后一个判断,如果obj是个对象怎么办?

function objectToPromise(obj){
// 构造一个和传入对象有相同构造器的对象, results也是
var results = new obj.constructor();
// 获取obj的keys
var keys = Object.keys(obj);
// 存储obj中是Promise的属性
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var promise = toPromise.call(this, obj[key]);
// 如果是结果是Promise,则用defer函数对results进行修改
if (promise && isPromise(promise)) defer(promise, key);
// 如果是非Promise就按原样返回
else results[key] = obj[key];
}
// 最后 使用到了Promise.all,将obj中多个promise实例
return Promise.all(promises).then(function () {
return results;
}); function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
// 运行成功之后再讲结果赋值给results
results[key] = res;
}));
}
}

结尾

到这里,co源码分析就告一段落了。总感觉有些没有说到位,欢迎大家拍砖,晚安。

走一步再走一步,揭开co的神秘面纱的更多相关文章

  1. ASP.NET 运行时详解 揭开请求过程神秘面纱

    对于ASP.NET开发,排在前五的话题离不开请求生命周期.像什么Cache.身份认证.Role管理.Routing映射,微软到底在请求过程中干了哪些隐秘的事,现在是时候揭晓了.抛开乌云见晴天,接下来就 ...

  2. SparkSQL大数据实战:揭开Join的神秘面纱

    本文来自 网易云社区 . Join操作是数据库和大数据计算中的高级特性,大多数场景都需要进行复杂的Join操作,本文从原理层面介绍了SparkSQL支持的常见Join算法及其适用场景. Join背景介 ...

  3. 带你揭开ATM的神秘面纱

    相信大家都用过ATM取过money吧,但是有多少人真正是了解ATM的呢?相信除了ATM从业者外了解的人寥寥无几吧,鄙人作为一个从事ATM软件开发的伪专业人士就站在我的角度为大家揭开ATM的神秘面纱吧. ...

  4. 揭开Future的神秘面纱——结果获取

    前言 在前面的两篇博文中,已经介绍利用FutureTask任务的执行流程,以及利用其实现的cancel方法取消任务的情况.本篇就来介绍下,线程任务的结果获取. 系列目录 揭开Future的神秘面纱—— ...

  5. 揭开Future的神秘面纱——任务执行

    前言 此文承接之前的博文 解开Future的神秘面纱之取消任务 补充一些任务执行的一些细节,并从全局介绍程序的运行情况. 系列目录 揭开Future的神秘面纱——任务取消 揭开Future的神秘面纱— ...

  6. 揭开HTTPS的神秘面纱

    摘自:https://www.cnblogs.com/hujingnb/p/11789728.html 揭开HTTPS的神秘面纱   在说HTTP前,一定要先介绍一下HTTP,这家伙应该不用过多说明了 ...

  7. 从一个Demo开始,揭开Netty的神秘面纱

    本文是Netty系列第5篇 上一篇文章我们对于I/O多路复用.Java NIO包 和 Netty 的关系有了全面的认识. 到目前为止,我们已经从I/O模型出发,逐步接触到了Netty框架.这个过程中, ...

  8. 揭开Future的神秘面纱——任务取消

    系列目录: 揭开Future的神秘面纱——任务取消 揭开Future的神秘面纱——任务执行 揭开Future的神秘面纱——结果获取 使用案例 在之前写过的一篇随笔中已经提到了Future的应用场景和特 ...

  9. 揭开Redis的神秘面纱

    本篇博文将为你解开Redis的神秘面纱,通过阅读本篇博文你将了解到以下内容: 什么是Redis? 为什么选择 Redis? 什么场景下用Redis? Redis 支持哪些语言? Redis下载 Red ...

随机推荐

  1. LeetCode-016-最接近的三数之和

    最接近的三数之和 题目描述:给定一个包括 n 个整数的数组 nums 和 一个目标值 target.找出 nums 中的三个整数,使得它们的和与 target 最接近.返回这三个数的和.假定每组输入只 ...

  2. JZ-007-斐波那契数列

    斐波那契数列 题目描述 大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1). n<=39 题目链接: 斐波那契数列 代码 publi ...

  3. 论文翻译:2020_DCCRN: Deep Complex Convolution Recurrent Network for Phase-Aware Speech Enhancement

    论文地址:DCCRN:用于相位感知语音增强的深度复杂卷积循环网络 论文代码:https://paperswithcode.com/paper/dccrn-deep-complex-convolutio ...

  4. tensorflow源码解析之framework拾遗

    把framework中剩余的内容,按照文件名进行了简单解析.时间原因写的很仓促,算是占个坑,后面有了新的理解再来补充. allocation_description.proto 一个对单次内存分配结果 ...

  5. MYSQL数年库安装

    MySQL系列 MySQL 的三大主要分支mysqlmariadbpercona Server MySQL系列2.2.2.1 MySQL 的三大主要分支mysqlmariadbpercona Serv ...

  6. Applied Social Network Analysis in Python 相关笔记

  7. VS Code配置Python环境

    Visual Studio Code配置Python环境 目录 Visual Studio Code配置Python环境 1.安装Python环境 2.安装VS Code 2.1 下载 2.2 配置中 ...

  8. springsecurity-01-0511

    springsecurity-01-0511课堂代码 BaseController package com.springsecurity.springsecurity.controller; impo ...

  9. CentOS 8 EOL如何切换源?

    镜像下载.域名解析.时间同步请点击 阿里巴巴开源镜像站 CentOS 8操作系统版本结束了生命周期(EOL),Linux社区已不再维护该操作系统版本.建议您切换到Anolis或Alinux.如果您的业 ...

  10. 冒泡排序和鸡尾酒排序(code)

    昨天回顾了下冒泡排序和鸡尾酒排序,用面向对象的方式写了一下,并且优化了代码,记录一下~ 一.冒泡排序 # 冒泡排序 class BubbleSort(object): def __init__(sel ...