本文始发于我的个人博客,如需转载请注明出处。

为了更好的阅读体验,可以直接进去我的个人博客看。

前言

知识储备

阅读本文需要对GeneratorPromise有一个基本的了解。

这里我简单地介绍一下两者的用法。

Generator

关于Generator的用法,推荐MDN上面的解释function *函数,里面非常详细。

用一句话总结就是,generator函数是回调地狱的一种解决方案,它跟promise类似,但是却可以以同步的方式来书写代码,而避免了promise的链式调用。

它的执行过程在于调用生成器函数(generator function)后,会返回一个iterator(迭代)对象,即Generator对象,但是它并不会立刻执行里面的代码。

它有几个方法,next(), throw()return()。调用next()方法后,它会找到第一个yield关键字(直到找到程序底部或者return语句),每次程序运行到yield关键字时,程序便会暂停,保存当前环境里面的变量的值,然后可以跳出当前运行环境去执行yield后面的代码,再把结果返回回来。

返回的结果是一个对象,类似于{value: '', done: false}, value表示本次yield后面执行之后返回的结果。如果是Promise实例,则是返回resolved后的值。done表示迭代器是否执行完毕,若为true,则表示当前生成器函数已经产生了最后输出的值,即生成器函数已经返回。

下面是一个简单的例子:

const gen = function *() {
let index = 0;
while(index < 3)
yield index++;
return 'All done.'
}; const g = gen();
console.log(g.constructor); // output: GeneratorFunction {}
console.log(g.next()); // output: { value: 0, done: false }
console.log(g.next()); // output: { value: 1, done: false }
console.log(g.next()); // output: { value: 2, done: false }
console.log(g.next()); // output: { value: 'All done.', done: true }
console.log(g.next()); // output: { value: undefined, done: true }

Promise

关于Promise的用法,可以查阅我之前写过的一篇文章《关于ES6中Promise的用法》,写得比较详细。

Promise对象用于一个异步操作的最终完成(或失败)及其结果值的表示(简单点说就是处理异步请求)。Promise核心就在于里面状态的变换,是rejectedresolved还是pending,还有就是原型链上的then()方法,它可以传递本次状态转换后返回的值。

进入主题

由于实际需要,这几天学习了koa2.x框架,但是它已经不推荐使用generator函数了,推荐用async/await组合。

koa2.x的最新用法:

async/await(node v7.6+):

const Koa = require('koa');
const app = new Koa(); app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

common 用法:

const Koa = require('koa');
const app = new Koa(); // response
app.use(ctx => {
ctx.body = 'Hello Koa';
}); app.listen(3000);

由于本地的Node版本是v6.11.5,而使用async/await则需要Node版本v7.6以上,所以我想有没有什么模块能够把koa2.x版本的语法兼容koa1.x的语法。koa1.x语法的关键在于generator/yield组合。通过yield可以很方便地暂停程序的执行,并改变执行环境。

这时候我找到了TJ大神写的co模块,它可以让异步流程同步化,还有koa-convert模块等等,这里着重介绍co模块。

co在koa2.x里面的用法如下:

const Koa = require('koa');
const app = new Koa();
const co = require('co'); // response
app.use(co.wrap(function *(ctx, next) {
yield next();
// yield someAyncOperation;
// ...
ctx.body = 'co';
})); app.listen(3000);

co模块不仅可以配合koa框架充当中间件的转换函数使用,还支持批量执行generator函数,这样就无需手动调用多次next()来获取结果了。

它支持的参数有函数、promise、generator、数组和对象

// co的源码
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传递进来一个generator函数的例子:

// 这里模拟一个generator函数调用
const co = require('co'); co(gen).then(data => {
// output: then: ALL Done.
console.log('then: ' + data);
}); function *gen() {
let data1 = yield pro1();
// output: pro1 had resolved, data1 = I am promise1
console.log('pro1 had resolved, data1 = ' + data1); let data2 = yield pro2();
// output: pro2 had resolved, data2 = I am promise2
console.log('pro2 had resolved, data2 = ' + data2); return 'ALL Done.'
} function pro1() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 2000, 'I am promise1');
});
} function pro2() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 'I am promise2');
});
}

我觉得co()函数很神奇,里面究竟经过了什么样的转换?抱着一颗好奇心,读了一下co的源码。

co源码分析

主要脉络

co函数调用后,返回一个Promise实例

co的思想就是将一个传递进来的参数进行合法化,再通过转换成Promise实例返回出去。如果参数fn是generator函数的话,里面还可以自动进行遍历,执行generator函数里面的yield关键字后面的内容,并返回结果,也就是不断地调用fn().next()方法,再通过传递返回的Promise实例resolved后的值,从而达到同步执行generator函数的效果。

这里要注意,co里面最主要的是要理解Promise实例和Generator对象,它们是co函数里面的程序自动遍历执行的关键

下面解释一下co模块里面的最重要的两部分,一个是generator函数的自动调用,另外一个是参数的Promise化

第一,generator函数的自动调用(中文部分是我的解释):

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 // 返回一个Promise实例
return new Promise(function(resolve, reject) {
// 如果gen是一个函数,则返回一个新的gen函数的副本,
// 里面绑定了this的指向,即ctx
if (typeof gen === 'function') gen = gen.apply(ctx, args); // 如果gen不存在或者gen.next不是一个函数
// 就说明gen已经调用完成,
// 那么直接可以resolve(gen),返回Promise
if (!gen || typeof gen.next !== 'function') return resolve(gen); // 首次调用gen.next()函数,假如存在的话
onFulfilled(); /**
* @param {Mixed} res
* @return {Promise}
* @api private
*/ function onFulfilled(res) {
var ret;
try {
// 尝试着获取下一个yield后面代码执行后返回的值
ret = gen.next(res);
} catch (e) {
return reject(e);
}
// 处理结果
next(ret);
} /**
* @param {Error} err
* @return {Promise}
* @api private
*/ function onRejected(err) {
var ret;
try {
// 尝试抛出错误
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
// 处理结果
next(ret);
} /**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/ // 这个next()函数是最为关键的一部分,
// 里面几乎包含了generator自动调用实现的核心
function next(ret) {
// 如果ret.done === true,
// 证明generator函数已经执行完毕
// 即已经返回了值
if (ret.done) return resolve(ret.value); // 把ret.value转换成Promise对象继续调用
var value = toPromise.call(ctx, ret.value); // 如果存在,则把控制权交给onFulfilled和onRejected,
// 实现递归调用
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) + '"'));
}
});
}

对于以上代码中的onFulfilledonRejected,我们可以把它们看成是co模块对于resolvereject封装的加强版。

第二,参数Promise化,我们来看一下co中的toPromise的实现:

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

toPromise的本质上就是通过判定参数的类型,然后再通过转移控制权给不同的参数处理函数,从而获取到期望返回的值。

关于参数的类型的判断,看一下源码就能理解了,比较简单。

我们着重来分析一下objectToPromise的实现:

function objectToPromise(obj){
// 获取一个和传入的对象一样构造器的对象
var results = new obj.constructor(); // 获取对象的所有可以遍历的key
var keys = Object.keys(obj);
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i]; // 对于数组的每一个项都调用一次toPromise方法,变成Promise对象
var promise = toPromise.call(this, obj[key]); // 如果里面是Promise对象的话,则取出e里面resolved后的值
if (promise && isPromise(promise)) defer(promise, key);
else results[key] = obj[key];
} // 并行,按顺序返回结果,返回一个数组
return Promise.all(promises).then(function () {
return results;
}); // 根据key来获取Promise实例resolved后的结果,
// 从而push进结果数组results中
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
}

上面理解的关键就在于把key遍历,如果key对应的value也是Promise对象的话,那么调用defer()方法来获取resolved后的值。

编写自己的generator函数运行器

通过以上的简单介绍,我们就可以尝试来写一个属于自己的generator函数运行器了,目标功能是能够自动运行function*函数,并且里面的yield子句后面跟着的都是Promise实例

具体代码(my-co.js)如下:

// my-co.js
module.exports = my-co; let my-co = function (gen) {
// gen是一个具有Promise的生成器函数
const g = gen(); // 迭代器 // 首次调用next
next(); function next(val) {
let ret = g.next(val); // 调用ret
if (ret.done) {
return ret.value;
} if (ret && 'function' === typeof ret.value.then) {
ret.value.then( (data) => {
// 继续循环下去
return next(data); // promise resolved
});
}
}
};

这样我们就可以在test.js文件中调用了:

// test.js
const myCo = require('./my-co');
const fs = require('fs'); let gen = function *() {
let data1 = yield pro1();
console.log('data1: ' + data1); let data2 = yield pro2();
console.log('data2: ' + data2); let data3 = yield pro3();
console.log('data3: ' + data3); let data4 = yield pro4(data1 + '\n' + data2 + '\n' + data3);
console.log('data4: ' + data4); return 'All done.'
}; // 调用myCo
myCo(gen); // 延迟两秒resolve
function pro1() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 2000, 'promise1 resolved');
});
} // 延迟一秒resolve
function pro2() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 'promise2 resolved');
});
} // 写入Hello World到./1.txt文件中
function pro3() {
return new Promise((resolve, reject) => {
fs.appendFile('./1.txt', 'Hello World\n', function(err) {
resolve('write-1 success');
});
});
} // 写入content到./1.txt文件中
function pro4(content) {
return new Promise((resolve, reject) => {
fs.appendFile('./1.txt', content, function(err) {
resolve('write-2 success');
});
});
}

控制台输出结果:

// output
data1: promise1 resolved
data2: promise2 resolved
data3: write-1 success
data4: write-2 success

./1.txt文件内容:

Hello World
promise1 resolved
promise2 resolved
write-1 success

由上可知,运行的结果符合我们的期望。

虽然这个运行器很简单,后面只支持Promise实例,并且也不支持多种参数,但是却引导出了一个思路,促使我们思考怎么去展示我们的代码,还有就是很有效地避免了多重then,以同步的方式来书写异步代码。Promise解决的是回调地狱的问题(callback hell),而Generator解决的是代码的书写方式。孰优孰劣,全在于个人意愿。

总结

以上分析了co部分源码的精髓,讲到了co函数里面generator函数自动遍历执行的机制,还讲到了co里面最为关键的objectToPromise()方法。

在文章的后面我们编写了一个属于自己的generator函数遍历器,其中主要的是next()方法,它可以检测我们yield后面Promise操作是否完成。如果generator的状态done还没有置为true,那么继续调用next(val)方法,并把上一次yield操作获取到的值传递下去。

有时候在引用别人的模块出现问题时,如果在网上找不到自己期望的答案,那么我们可以根据自己的能力来选择性地分析一下作者的源码,看源码是一种很好的成长方式

坦白说,这是我第一次深入分析模块的源码,co模块的源码包括注释和空行只有230多行左右,所以这是一个很好的切入点。里面代码虽少,但是理解却不易。

如果以上所述有什么问题,欢迎反馈。

感谢支持。

参考链接

  1. MDN - Promise解释

  2. MDN - Generator对象的用法

  3. TJ - co的源码及其用法

co源码分析及其实践的更多相关文章

  1. 自定义View系列教程04--Draw源码分析及其实践

    深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...

  2. ThreadLocal源码分析与实践

    ThreadLocal是什么? ThreadLocal是一个线程内部存储类,提供线程内部存储功能,在一个ThreadLocal对象中,每一个线程都存储各自独立的数据,互不干扰 示例如下: public ...

  3. Go合集,gRPC源码分析,算法合集

    年初时,朋友圈见到的最多的就是新的一年新的FlAG,年末时朋友圈最多的也是xxxx就要过去了,你的FLAG实现了吗? 这个公众号2016就已经创建了,但截至今年之前从来没发表过文章,现在想想以前很忙, ...

  4. netty源码分析(十八)Netty底层架构系统总结与应用实践

    一个EventLoopGroup当中会包含一个或多个EventLoop. 一个EventLoop在它的整个生命周期当中都只会与唯一一个Thread进行绑定. 所有由EventLoop所处理的各种I/O ...

  5. spring security 实践 + 源码分析

    前言 本文将从示例.原理.应用3个方面介绍 spring data jpa. 以下分析基于spring boot 2.0 + spring 5.0.4版本源码 概述 Spring Security 是 ...

  6. java使用websocket,并且获取HttpSession,源码分析

    转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/6238826.html 一:本文使用范围 此文不仅仅局限于spring boot,普通的sprin ...

  7. Tomcat源码分析

    前言: 本文是我阅读了TOMCAT源码后的一些心得. 主要是讲解TOMCAT的系统框架, 以及启动流程.若有错漏之处,敬请批评指教! 建议: 毕竟TOMCAT的框架还是比较复杂的, 单是从文字上理解, ...

  8. Java并发包源码学习之AQS框架(四)AbstractQueuedSynchronizer源码分析

    经过前面几篇文章的铺垫,今天我们终于要看看AQS的庐山真面目了,建议第一次看AbstractQueuedSynchronizer 类源码的朋友可以先看下我前面几篇文章: <Java并发包源码学习 ...

  9. Mahout源码分析之 -- 文档向量化TF-IDF

    fesh个人实践,欢迎经验交流!Blog地址:http://www.cnblogs.com/fesh/p/3775429.html Mahout之SparseVectorsFromSequenceFi ...

随机推荐

  1. centOS 6 服务管理与服务脚本

    服务管理与服务脚本   linux服务 服务管理与服务脚本 linux服务 服务启动过程详解 chkconfig命令 非独立服务与xinetd进程 一个特殊的服务脚本   服务启动过程详解 在开机启动 ...

  2. ThinkJS框架入门详细教程(一)开发环境

    一.前端标配环境 1.nodeJS正确安装,可以参考:http://www.cnblogs.com/chengxs/p/6221393.html 2.git正确安装,可以参考:http://www.c ...

  3. MySQL高级查询(二)

    EXISTS 和NOT EXISTS子查询 EXISTS子查询 语法:   SELECT ……… FROM 表名 WHERE EXISTS (子查询); 例: SELECT `studentNo` A ...

  4. JS设计模式(一) 单例模式

    命名空间 单例模式是JavaScript中最常见的一种模式,通过这种模式可以为我们提供一个命名空间,例如jQuery库的命名空间为jQuery或$.命名空间的使用是为了让代码更加整洁,在多人协作开发的 ...

  5. JAVA对象头

    #为了防止自己忘记,先记着,之前我一直以为<深入理解JAVA虚拟机>写错了来着. 一. JAVA对象 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header ...

  6. JSP入门 Filter

    Filter,它的名字是过滤器,可以批量拦截修改servlet的请求和响应. 需要实现接口Filter 配置 <filter> <filter-name>EncodingFil ...

  7. Spring之注解实现aop(面向切面编程)

    1:Aop(aspect object programming)面向切面编程,名词解释:    1.1:功能:让关注点代码与业务逻辑代码分离    1.2:关注点        重复代码就叫做关注点  ...

  8. NOIP2017SummerTraining0713

    个人感受:这套题是真的难,以至于,拿了130分就第三了(说来羞耻,真的不想---) 问题 A: 乐曲创作 时间限制: 1 Sec  内存限制: 256 MB提交: 370  解决: 58[提交][状态 ...

  9. 决策树模型组合之随机森林与GBDT

    版权声明: 本文由LeftNotEasy发布于http://leftnoteasy.cnblogs.com, 本文可以被全部的转载或者部分使用,但请注明出处,如果有问题,请联系wheeleast@gm ...

  10. java基础知识及详细笔记

    第一章:初识java 1.1.java的概述 ü  什么是计算机程序:计算机按照某种顺序而完成的一系列有序指令的集合. ü  Java的作用:1:安装和运行本机上的桌面程序.2:通过浏览器访问面向in ...