原文地址:http://www.moye.me/2014/11/10/ecmascript-6-generator/

引子

老听人说 koa大法好,这两天我也赶了把时髦:用 n 安上了node 0.11.12,下了个koa开启harmony模式试水。在一系列文档和贴子的教育下,大概认识到:

  • koa 是TJ大神主导的新一代Web框架
  • koa 的中间件基于ES6的生成器函数(function *)形式
  • koa的核心流程库是 co,它能很好的解决Pyramid of Doom问题

在接触 Node.js 前,由于有过 Python编程的经验,我对生成器是个什么东西已经是很清楚了。我真正感兴趣的是:它是怎么被用来优化回调嵌套的。

“为何这么屌”

在 KOA框架为何这么屌 一文中有这么一段(片断1):

  1. var fs = require('fs');
  2. var app = require('koa')();
  3. var readFile = function(dir) {
  4. return function(fn) {
  5. fs.readFile(dir, fn);
  6. }
  7. }
  8. app.use(function* () {
  9. var arr = yield ['1.txt', '2.txt', '3.txt'].map(function(path) {
  10. return readFile(path);
  11. });
  12. this.body = arr.join(',');
  13. })
  14. app.listen(8000);

这段代码很好的演示了koa是如何利用生成器函数(function*(),函数的constructor.name === 'GeneratorFunction' )来串行化异步回调的,它的执行流程:

  1. function*(){...} 被做为生成器函数push到了koa的中间件队列中
  2. koa使用co框架,对这个生成器函数进行调用执行;执行生成器函数并不立即执行函数体,而是生成生成器(generator)实例——同时,生成器可视为遵循迭代器协议的实例,每次调用迭代器的next(),都会返回一个{ value: obj, done:true/false }对象:value是执行结果值,done指示迭代是否完成
  3. 调用生成器实例的next()方法将启动函数体内部的执行流,直到出现yield时被挂起,那么下一次的next()将会给yield返回执行的结果值,并挂起在下一个yield出现处;可以理解为,yield总是返回上一次next()的结果值,如果next()有参数,yield将返回这个参数值(异步回调有机会注入执行结果)
  4. 观察上面代码片段我们注意到,readFile的回调处理函数fn代码中并未提供,那么koa或者说co是怎么处理这个回调函数的呢?在 koa(0.13.0)自带的co源码(line:84)中,可以看到如下片断(片断2):
    1. // normalize
    2. ret.value = toThunk(ret.value, ctx);
    3. // run
    4. if ('function' == typeof ret.value) {
    5. var called = false;
    6. try {
    7. ret.value.call(ctx, function(){
    8. if (called) return;
    9. called = true;
    10. next.apply(ctx, arguments);
    11. });
    12. } catch (e) {
    13. setImmediate(function(){
    14. if (called) return;
    15. called = true;
    16. next(e);
    17. });
    18. }
    19. return;
    20. }
  5. toThunk 会根据 yield返回的表达式转换成标准函数(片断3):
    1. function toThunk(obj, ctx) {
    2. if (isGeneratorFunction(obj)) {
    3. return co(obj.call(ctx));
    4. }
    5. if (isGenerator(obj)) {
    6. return co(obj);
    7. }
    8. if (isPromise(obj)) {
    9. return promiseToThunk(obj);
    10. }
    11. if ('function' == typeof obj) {
    12. return obj;
    13. }
    14. if (isObject(obj) || Array.isArray(obj)) {
    15. return objectToThunk.call(ctx, obj);
    16. }
    17. return obj;
    18. }
  6. 片断1 中,生成器函数首先返回的是一个生成器;然后,yield 结合map会返回三个function对象,即高阶函数readFile返回的function:
    1. function(fn) {
    2. fs.readFile(dir, fn);
    3. }
  7. 片断2 根据 片断3 返回的类型,对 function 进行了call调用,并提供了回调函数:将arguments通过next.apply(ctx, arguments);巧妙的进行传递。如前所述,next()如果提供了参数,yield得到的结果值就是这个参数,回调结果由此而来。

到底是谁屌

如果看官看完上面那几段还没晕,那当然是您最屌:) ——我的表述能力确实不足以很清晰的道出框架的玄机,但在我看来,真正屌的是ES6 Generator机制本身。

暂时放下 co 框架,把 片断1 稍加改造(片断4):

  1. var fs = require('fs');
  2. var path = require('path');
  3.  
  4. var readFile = function (dir) {
  5. return function (fn) {
  6. fs.readFile(dir, {encoding: 'utf8', flag: 'r'}, fn);
  7. };
  8. };
  9.  
  10. function *readFileGeneratorFunction(path, cb){
  11. console.log(yield readFile(path)(cb));
  12. }
  13.  
  14. var readFileIterator = readFileGeneratorFunction('testDate.js', callback);
  15. function callback(err, data){
  16. readFileIterator.next(data);
  17. }
  18. readFileIterator.next();

用意很明显:

  1. 这个readFileGeneratorFunction就是个生成器函数,执行它返回一个生成器(迭代器)
  2. 高阶函数返回的function,在生成器函数执行时指定了回调
  3. next触发执行
  4. 回调完成时,next(data)携带结果值触发yield

问题也很明显,业务代码(GeneratorFunction中的yield) 需要前置于流程控制(callback),这不科学。抽象一下,可以提供一个生成器函数的执行函数:

  1. var slice = Array.prototype.slice;
  2. function run(generatorFunction) {
  3. try {
  4. var generatorItr = generatorFunction(callback);
  5. function callback(err, res) {
  6. if(err)
  7. generatorItr.throw(err);
  8. else {
  9. var args = slice.call(arguments, 1);
  10. res = args.length > 1 ? args : res;
  11. generatorItr.next(res);
  12. }
  13. }
  14. generatorItr.next();
  15. }
  16. catch (e){
  17. console.log(e.message | "I'm died.");
  18. }
  19. };
  20.  
  21. 测试一下:
  22.  
  23. run(function* rfGenFunc(cb) {
  24. console.log('first');
  25. console.log(yield readFile('1.txt')(cb));
  26. console.log('second');
  27. console.log(yield readFile('2.txt')(cb));
  28. });

执行结果:

小结

本文仅对Generator的next()应用进行了简单的描述(其实它还有更多内容如:throw/send/close),抛砖引玉罢了。至于生成器特性,目前仍处于 ECMAScript 6 规范草案中,如MDN所言:请谨慎使用 :)

更多文章请移步我的blog新地址: http://www.moye.me/

[Node.js] ECMAScript 6中的生成器及koa小析的更多相关文章

  1. Elasticsearch.js 发布 —— 在Node.js和浏览器中调用Elasticsearch(1)

    继PHP.Ruby.Python和Perl之后,Elasticsearch最近发布了Elasticsearch.js,Elasticsearch的JavaScript客户端库.可以在Node.js和浏 ...

  2. Elasticsearch.js 发布 —— 在Node.js和浏览器中调用Elasticsearch

    继PHP.Ruby.Python和Perl之后,Elasticsearch最近发布了Elasticsearch.js,Elasticsearch的JavaScript客户端库.可以在Node.js和浏 ...

  3. Node.js开发指南中的例子(mysql版)

    工作原因需要用到nodejs,于是找到了<node.js开发指南>这本书来看看,作者BYVoid 为清华大学计算机系的高材生,年纪竟比我还小一两岁,中华地广物博真是人才辈出,佩服. 言归正 ...

  4. node.js获取url中的各个参数

    实例代码test.js var http=require('http'); var url=require('url'); var querystring=require('querystring') ...

  5. 高效使用 JavaScript 闭包,避免 Node.js 应用程序中的内存泄漏

    在 Node.js 中,广泛采用不同形式的闭包来支持 Node 的异步和事件驱动编程模型.通过很好地理解闭包,您可以确保所开发应用程序的功能正确性.稳定性和可伸缩性. 闭包是一种将数据与处理数据的代码 ...

  6. Node.js向MongoDB中插入并查询数据

    首先必须要保持Node.js与MongoDB保持连接 具体教程见:Node.js连接MongoDB数据库步骤 插入数据步骤如下 node项目文件如下:在routes文件夹下新建insert.js文件, ...

  7. Koa与Node.js开发实战(2)——使用Koa中间件获取响应时间(视频演示)

    学习架构: 在实战项目中,经常需要记录下服务器的响应时间,也就是从服务器接收到HTTP请求,到最终返回给客户端之间所耗时长.在Koa应用中,利用中间件机制可以很方便的实现这一功能.代码如下所示: 01 ...

  8. Node.js躬行记(19)——KOA源码分析(上)

    本次分析的KOA版本是2.13.1,它非常轻量,诸如路由.模板等功能默认都不提供,需要自己引入相关的中间件. 源码的目录结构比较简单,主要分为3部分,__tests__,lib和docs,从名称中就可 ...

  9. Node.js躬行记(20)——KOA源码分析(下)

    在上一篇中,主要分析了package.json和application.js文件,本文会分析剩下的几个文件. 一.context.js 在context.js中,会处理错误,cookie,JSON格式 ...

随机推荐

  1. 深入理解.NET程序的原理 谈一谈破解.NET软件的工具和方法

    最近一段时间不忙,闲下来的空闲时间,重读了一下CLR的原理,回味一下有关程序集的的知识,顺便练了一下手,学习致用,破解了若干个.NET平台的软件.以此来反观.NET程序开发中,需要注意的一些问题. 基 ...

  2. Android SDK Tools Platform-tools Build-tools

    (1)Android SDK (Android SDK主安装包,包含SDK Manager.AVD Manager.工具包tools,释放后的根文件夹为android-sdk-windows): re ...

  3. Scala 具体的并行集合库【翻译】

    原文地址 本文内容 并行数组(Parallel Array) 并行向量(Parallel Vector) 并行范围(Parallel Range) 并行哈希表(Parallel Hash Tables ...

  4. int跟byte[]数组互转的方法,整数 + 浮点型

    整数: int转byte数组 public static byte[] intToBytes2(int n){ ]; ;i < ;i++) { b[i]=(-i*)); } return b; ...

  5. 如何导入大sql文件到虚拟主机mysql数据库

    大部分网站虚拟主机为了安全起见,都限制了通过命令或者phpMyAdmin导入大sql文件到mysql数据库,例如godaddy只允许站长通过phpMyAdmin上传不超过2m的sql文件,但实际上我们 ...

  6. 阿里云 Redis 服务遇到的问题

    ERR unknown command eval 说明: 执行当前 Web 请求期间,出现未经处理的异常.请检查堆栈跟踪信息,以了解有关该错误以及代码中导致错误的出处的详细信息. 异常详细信息: St ...

  7. No connection string named '***' could be found in the application config file

    Code-First时更新数据库遇到妖孽问题“No connection string named '***' could be found in the application config fil ...

  8. Rxlifecycle(三):坑

    坑1 Observable.just("hello world!") .compose(this.<String>bindUntilEvent(ActivityEven ...

  9. friend class

    友元函数与友元类.   C++中以关键字friend声明友元关系.友元可以访问与其有friend关系的类中的私有成员.友元包括友元函数和友元类.   编辑本段1.友元函数 如果在本类以外的其它地方定义 ...

  10. 严重: Exception starting filter struts2解决方法!

    转自:http://blog.knowsky.com/260578.htm 问题出现: 严重: Exception starting filter struts2java.lang.ClassNotF ...