前言

原文地址

源码地址

了解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. C++二维动态数组

    //创建 int **a=new int *[n]; for(i=0;i<n;i++) a[i]=new int[n]; // -- // 删除 for(i=0;i<n;i++) dele ...

  2. WPS:添加公式后,行间距变宽的解决方法

    找到公式所属段落的样式,右键修改样式 左下角'格式'中选择'段落' 段落间距设置为0,不要勾选与文档网格对齐

  3. php简易表单及下拉框动态渲染

    <?php//1.连接数据库$link = mysqli_connect('127.0.0.1','root','root','1906');//2.设置字符集mysqli_set_charse ...

  4. PHP pdf转png linux版本

    linux下 安装 ImageMagick 及其 php imagick扩展 PHP版本 7.1.29 : ImageMagick版本 ImageMagick-7.0.8-68: PHP扩展imagi ...

  5. XSS攻击防范

    前端安全系列之XSS攻击防范 1.使用textContent 2.使用HTML转义 把JS中的标签转成字符 3.对于链接跳转 禁止含有'javascript:'开头的字符 4.标签属性中含有恶意执行代 ...

  6. idea Mybatis mapper配置文件删除SQL语句背景色

    原样式: 看着很不爽 本文 idea 版本为:idea 2020.3.1,以下操作均基于此版本进行 解决办法 1.去除警告 Settings>Editor>Inspections>S ...

  7. vite2 打包的时候vendor-xxx.js文件过大的解决方法

    vite2是一个非常好用的工具,只是随着代码的增多,打包的时候 vendor-xxxxxx.js 文件也越来越大,这就郁闷了. 打包时遇到警告 输出文件名字/static/vendor.9b5698e ...

  8. VUE3 之 状态动画 - 这个系列的教程通俗易懂,适合新手

    1. 概述 老话说的好:不用羡慕别人,每个人都有属于自己的人生道路,重要的是在前进道路上遇见阻碍时,如何去积极的面对并解决. 言归正传,今天我们来聊聊 VUE 的状态动画. 2. 状态动画 2.1 数 ...

  9. mongoDb入门并整合springboot

    镜像下载.域名解析.时间同步请点击 阿里巴巴开源镜像站 MongoDb简介 MongoDb是一种非关系型数据库,是现在非常火热的noSQL.也被称为文档性数据库.(可存放json,xml等格式) mo ...

  10. Ubuntu20.04服务器+Anaconda上创建Python3.6虚拟环境并

    镜像下载.域名解析.时间同步请点击 阿里巴巴开源镜像站 前言 由于服务器已安装Anaconda,包含的Python版本为3.8,为使用3.6版本同时避免和其他人互相影响,我选择创建虚拟环境,并在其中安 ...