走一步再走一步,揭开co的神秘面纱
前言
了解
co
的前提是已经知晓generator
是什么,可以看软大神的Generator 函数的语法,
co是TJ大神写的能够使generator自动执行的函数库,而我们熟知的koa也用到了它管理异步流程控制,将异步任务书写同步化,爽的飞起,也摆脱了一直以来的回调地狱问题。
如何使用
首先我们根据co的官方文档来稍做改变看下,到底如何使用co,再一步步进行源码分析工作(这篇文章分析的co版本是
4.6.0
)。
yield 后面常见的可以跟的类型
promises
array (parallel execution)
objects (parallel execution)
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);
}
我觉得可以把 onFulfilled
和 onRejected
看成是返回的Promise的resolve
和reject
。
而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的神秘面纱的更多相关文章
- ASP.NET 运行时详解 揭开请求过程神秘面纱
对于ASP.NET开发,排在前五的话题离不开请求生命周期.像什么Cache.身份认证.Role管理.Routing映射,微软到底在请求过程中干了哪些隐秘的事,现在是时候揭晓了.抛开乌云见晴天,接下来就 ...
- SparkSQL大数据实战:揭开Join的神秘面纱
本文来自 网易云社区 . Join操作是数据库和大数据计算中的高级特性,大多数场景都需要进行复杂的Join操作,本文从原理层面介绍了SparkSQL支持的常见Join算法及其适用场景. Join背景介 ...
- 带你揭开ATM的神秘面纱
相信大家都用过ATM取过money吧,但是有多少人真正是了解ATM的呢?相信除了ATM从业者外了解的人寥寥无几吧,鄙人作为一个从事ATM软件开发的伪专业人士就站在我的角度为大家揭开ATM的神秘面纱吧. ...
- 揭开Future的神秘面纱——结果获取
前言 在前面的两篇博文中,已经介绍利用FutureTask任务的执行流程,以及利用其实现的cancel方法取消任务的情况.本篇就来介绍下,线程任务的结果获取. 系列目录 揭开Future的神秘面纱—— ...
- 揭开Future的神秘面纱——任务执行
前言 此文承接之前的博文 解开Future的神秘面纱之取消任务 补充一些任务执行的一些细节,并从全局介绍程序的运行情况. 系列目录 揭开Future的神秘面纱——任务取消 揭开Future的神秘面纱— ...
- 揭开HTTPS的神秘面纱
摘自:https://www.cnblogs.com/hujingnb/p/11789728.html 揭开HTTPS的神秘面纱 在说HTTP前,一定要先介绍一下HTTP,这家伙应该不用过多说明了 ...
- 从一个Demo开始,揭开Netty的神秘面纱
本文是Netty系列第5篇 上一篇文章我们对于I/O多路复用.Java NIO包 和 Netty 的关系有了全面的认识. 到目前为止,我们已经从I/O模型出发,逐步接触到了Netty框架.这个过程中, ...
- 揭开Future的神秘面纱——任务取消
系列目录: 揭开Future的神秘面纱——任务取消 揭开Future的神秘面纱——任务执行 揭开Future的神秘面纱——结果获取 使用案例 在之前写过的一篇随笔中已经提到了Future的应用场景和特 ...
- 揭开Redis的神秘面纱
本篇博文将为你解开Redis的神秘面纱,通过阅读本篇博文你将了解到以下内容: 什么是Redis? 为什么选择 Redis? 什么场景下用Redis? Redis 支持哪些语言? Redis下载 Red ...
随机推荐
- 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 ...
- JDK下载安装与环境变量配置【全网最新】
1.下载安装JDK 下载地址:(https://www.oracle.com/java/technologies/downloads/) 最好选择解压版,解压即可(说删就删) 解压:例如我解压目录为 ...
- appium ios 相对坐标点击和控件滑动操作
环境: 系统:ios 10.13.6 (17G12034) appium:1.14.0 xcode:10.1 iphone:iphone7 12.4 在尝试使用driver.tap([(a,b)],5 ...
- 快速整透Redis中的压缩列表到底是个啥
压缩列表简介 压缩列表(ziplist)是由一个连续内存组成的顺序型数据结构.一个压缩列表可以包含任意多个节点,每个节点上可以保存一个字节数组或整数值.它是Redis为了节省内存空间而开发的. 压缩列 ...
- css样式之浮动
什么是浮动? 添加了浮动的的元素会脱离正常的文档流. 浮动的特点: 1.可以让块级元素排在同一排 2.可以让行属性标签支持所有的css样式 3.遇到相邻的浮动元素或者父级元素会停下来 4.浮动会影响其 ...
- 理解并手写 bind() 函数
有了对call().apply()的前提分析,相信bind()我们也可以手到擒来. 参考前两篇:'对call()函数的分析' 和 '对apply()函数的分析',我们可以先得到以下代码: Functi ...
- 打靶笔记-03-vulhub-Moriarty Corp
打靶笔记-03-vulhub-BoredHackerBlog 一.靶机信息 Name: BoredHackerBlog: Moriarty Corp(中-高级难度) Date release: 29 ...
- 福利|GISer需知网站
一些GISer需知的网站推荐 1.https://www.usgs.gov/ 美国地质调查局 美国地质勘探局(United States Geological Survey,简称USGS),又译美国地 ...
- npm vue项目的创建
一.创建项目之前需要先下载一个node.js 官方网址:https://nodejs.org/en/ 二.创建 (1)建一个文件夹,进入这个文件夹输入cmd打开小黑窗: Vue.js文档:https ...
- JavaWeb 08_JSP+Dao+Bean+Servlet 实现登录注册(连接数据库,验证码登录,两周内免登陆等功能)
一.数据库db_01 表usert 字段username,password 二. 目录 三. 配置信息 四. 代码 index.jsp <script type="text/j ...