前言

原文地址

源码地址

了解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. hexo部署到github时,提示typeError [ERR_INVALID_ARG_TYPE] The “mode“ argument must be integer. Receive

    hexo部署到github时,提示typeError [ERR_INVALID_ARG_TYPE]: The "mode" argument must be integer. Re ...

  2. JDK下载安装与环境变量配置【全网最新】

    1.下载安装JDK 下载地址:(https://www.oracle.com/java/technologies/downloads/) 最好选择解压版,解压即可(说删就删) 解压:例如我解压目录为 ...

  3. appium ios 相对坐标点击和控件滑动操作

    环境: 系统:ios 10.13.6 (17G12034) appium:1.14.0 xcode:10.1 iphone:iphone7 12.4 在尝试使用driver.tap([(a,b)],5 ...

  4. 快速整透Redis中的压缩列表到底是个啥

    压缩列表简介 压缩列表(ziplist)是由一个连续内存组成的顺序型数据结构.一个压缩列表可以包含任意多个节点,每个节点上可以保存一个字节数组或整数值.它是Redis为了节省内存空间而开发的. 压缩列 ...

  5. css样式之浮动

    什么是浮动? 添加了浮动的的元素会脱离正常的文档流. 浮动的特点: 1.可以让块级元素排在同一排 2.可以让行属性标签支持所有的css样式 3.遇到相邻的浮动元素或者父级元素会停下来 4.浮动会影响其 ...

  6. 理解并手写 bind() 函数

    有了对call().apply()的前提分析,相信bind()我们也可以手到擒来. 参考前两篇:'对call()函数的分析' 和 '对apply()函数的分析',我们可以先得到以下代码: Functi ...

  7. 打靶笔记-03-vulhub-Moriarty Corp

    打靶笔记-03-vulhub-BoredHackerBlog 一.靶机信息 Name: BoredHackerBlog: Moriarty Corp(中-高级难度) Date release: 29 ...

  8. 福利|GISer需知网站

    一些GISer需知的网站推荐 1.https://www.usgs.gov/ 美国地质调查局 美国地质勘探局(United States Geological Survey,简称USGS),又译美国地 ...

  9. npm vue项目的创建

    一.创建项目之前需要先下载一个node.js 官方网址:https://nodejs.org/en/  二.创建 (1)建一个文件夹,进入这个文件夹输入cmd打开小黑窗: Vue.js文档:https ...

  10. JavaWeb 08_JSP+Dao+Bean+Servlet 实现登录注册(连接数据库,验证码登录,两周内免登陆等功能)

    一.数据库db_01   表usert   字段username,password 二. 目录 三. 配置信息 四. 代码 index.jsp <script type="text/j ...