资讯专栏INFORMATION COLUMN

Javascript Generator - 函数式编程 - Javascript核心

yearsj / 3360人阅读

摘要:中的的引入,极大程度上改变了程序员对迭代器的看法,并为解决提供了新方法。被称为,也有些人把的返回值称为一个。其中属性包含实际返回的数值,属性为布尔值,标记迭代器是否完成迭代。

  

原文: http://pij.robinqu.me/JavaScript_Core/Functional_JavaScript/JavaScript_Generator.html

  

源代码: https://github.com/RobinQu/Programing-In-JavaScript/blob/master/chapters/JavaScript_Core/Functional_JavaScript/JavasSript_Generator.md

本文需要补充更多例子

本文存在批注,但该网站的Markdown编辑器不支持,所以无法正常展示,请到原文参考。

Javascript Generator

ES6中的Generator的引入,极大程度上改变了Javascript程序员对迭代器的看法,并为解决callback hell1提供了新方法。

Generator是一个与语言无关的特性,理论上它应该存在于所有Javascript引擎内,但是目前真正完整实现的,只有在node --harmony 下。所以后文所有的解释,都以node环境举例,需要的启动参数为node --harmony --use_strict

V8中所实现的Generator和标准之中说的又有区别,这个可以参考一下MDC的相关文档2。而且,V8在写作这篇文章时,并没有实现Iterator。

用作迭代器

我们以一个简单的例子3开始:

function* argumentsGenerator() {
  for (let i = 0; i < arguments.length; i += 1) {
    yield arguments[i];
  }
}

我们希望迭代传入的每个实参:

var argumentsIterator = argumentsGenerator("a", "b", "c");

// Prints "a b c"
console.log(
    argumentsIterator.next().value,
    argumentsIterator.next().value,
    argumentsIterator.next().value
);

我们可以简单的理解:

Generator其实是生成Iterator的方法。argumentsGenerator被称为GeneartorFunction,也有些人把GeneartorFunction的返回值称为一个Geneartor

yield可以中断GeneartorFunction的运行;而在下一次yield时,可以恢复运行。

返回的Iterator上,有next成员方法,能够返回迭代值。其中value属性包含实际返回的数值,done属性为布尔值,标记迭代器是否完成迭代。要注意的是,在done属性为true后继续运行next方法会产生异常。

完整的ES实现中,for-of循环正是为了快速迭代一个iterator的:

// Prints "a", "b", "c"
for(let value of argumentsIterator) {
  console.log(value);
}

可惜,目前版本的node不支持for-of

说到这里,大多数有经验的Javascript程序员会表示不屑,因为这些都可以通过自己编写一个函数来实现。我们再来看一个例子:

function* fibonacci() {
  let a = 0, b = 1;
  //1, 2
  while(true) {
    yield a;
    a = b;
    b = a + b;
  }
}

for(let value of fibonacci()) {
  console.log(value);
}

fibonacci序列是无穷的数字序列,你可以用函数的迭代来生成,但是远没有用Generator来的简洁。

再来个更有趣的。我们可以利用yield*语法,将yield操作代理到另外一个Generator

let delegatedIterator = (function* () {
  yield "Hello!";
  yield "Bye!";
}());

let delegatingIterator = (function* () {
  yield "Greetings!";
  yield* delegatedIterator;
  yield "Ok, bye.";
}());

// Prints "Greetings!", "Hello!", "Bye!", "Ok, bye."
for(let value of delegatingIterator) {
  console.log(value);
}
用作流程控制

yield可以暂停运行流程,那么便为改变执行流程提供了可能4。这和Python的coroutine类似。

co已经将此特性封装的非常完美了。我们在这里简单的讨论其实现。

  

The classic example of this is consumer-producer relationships: generators that produce values, and then consumers that use them. The two generators are said to be symmetric – a continuous evaluation where coroutines yield to each other, rather than two functions that call each other.

Geneartor之所以可用来控制代码流程,就是通过yield来将两个或者多个Geneartor的执行路径互相切换。这种切换是语句级别的,而不是函数调用级别的。其本质是CPS变幻,后文会给出解释。

这里要补充yield的若干行为:

next方法接受一个参数,传入的参数是yield表达式的返回值;即yield既可以产生数值,也可以接受数值

throw方法会抛出一个异常,并终止迭代

GeneratorFunction的return语句等同于一个yield

将异步“变”为同步

假设我们希望有如下语法风格:

suspend传入一个GeneratorFunction

suspend返回一个简单的函数,接受一个node风格的回调函数

所有的异步调用都通过yield,看起来像同步调用

给定一个特殊的回调,让保证异步调用的返回值作为yield的返回值,并且让脚本继续

GeneratorFunction的返回值和执行过程的错误都会会传入全局的回调函数

更具体的,如下例子:

var fs = require("fs");
suspend(function*(resume) {
  var content = yield fs.readFile(__filename, resume);
  var list = yield fs.readdir(__dirname, resume);
  return [content, list];
})(function(e, res) {
  console.log(e,res);
});

上面分别进行了一个读文件和列目录的操作,均是异步操作。为了实现这样的suspendresume。我们简单的封装Generator的API:

var slice = Array.prototype.slice.call.bind(Array.prototype.slice);

var suspend = function(gen) {//`gen` is a generator function
  return function(callback) {
    var args, iterator, next, ctx, done;
    ctx = this;
    args = slice(arguments);

    next = function(e) {
      if(e) {//throw up or send to callback
        return callback ? callback(e) : iterator.throw(e);
      }
      var ret = iterator.next(slice(arguments, 1));
      if(ret.done && callback) {//run callback is needed
        callback(null, ret.value);
      }
    };

    resume = function(e) {
      next.apply(ctx, arguments);
    };

    args.unshift(resume);
    iterator = gen.apply(this, args);
    next();//kickoff
  };
};
有容乃大

目前我们只支持回调形势的API,并且需要显示的传入resume作为API的回调。为了像co那样支持更多的可以作为yield参数。co中,作者将所有形势的异步对象都归结为一种名为thunk的回调形式。

那什么是thunk呢?thunk就是支持标准的node风格回调的一个函数: fn(callback)

首先我们将suspend修改为自动resume:

var slice = Array.prototype.slice.call.bind(Array.prototype.slice);

var suspend = function(gen) {
  return function(callback) {
    var args, iterator, next, ctx, done;
    ctx = this;
    args = slice(arguments);
    next = function(e) {
      if(e) {
        return callback ? callback(e) : iterator.throw(e);
      }
      var ret = iterator.next(slice(arguments, 1));

      if(ret.done && callback) {
        return callback(null, ret.value);
      }

      if("function" === typeof ret.value) {//shold yield a thunk
        ret.value.call(ctx, function() {//resume function
          next.apply(ctx, arguments);
        });
      }

    };

    iterator = gen.apply(this, args);
    next();
  };
};

注意,这个时候,我们只能yield一个thunk,我们的使用方法也要发生改变:

var fs = require("fs");
read = function(filename) {//wrap native API to a thunk
  return function(callback) {
    fs.readFile(filename, callback);
  };
};

suspend(function*() {//return value of this generator function is passed to callback
  return yield read(__filename);
})(function(e, res) {
  console.log(e,res);
});

接下来,我们要让这个suspend更加有用,我们可以支持如下内容穿入到yield

GeneratorFunction

Generator

Thunk

var slice = Array.prototype.slice.call.bind(Array.prototype.slice);

var isGeneratorFunction = function(obj) {
  return obj && obj.constructor && "GeneratorFunction" == obj.constructor.name;
};

var isGenerator = function(obj) {
  return obj && "function" == typeof obj.next && "function" == typeof obj.throw;
};

var suspend = function(gen) {
  return function(callback) {
    var args, iterator, next, ctx, done, thunk;
    ctx = this;
    args = slice(arguments);
    next = function(e) {
      if(e) {
        return callback ? callback(e) : iterator.throw(e);
      }
      var ret = iterator.next(slice(arguments, 1));

      if(ret.done && callback) {
        return callback(null, ret.value);
      }

      if(isGeneratorFunction(ret.value)) {//check if it"s a generator
        thunk = suspend(ret.value);
      } else if("function" === typeof ret.value) {//shold yield a thunk
        thunk = ret.value;
      } else if(isGenerator(ret.value)) {
        thunk = suspend(ret.value);
      }

      thunk.call(ctx, function() {//resume function
        next.apply(ctx, arguments);
      });

    };

    if(isGeneratorFunction(gen)) {
      iterator = gen.apply(this, args);
    } else {//assume it"s a iterator
      iterator = gen;
    }
    next();
  };
};

在使用时,我们可以传入三种对象到yield:

var fs = require("fs");
read = function(filename) {
  return function(callback) {
    fs.readFile(filename, callback);
  };
};

var read1 = function*() {
  return yield read(__filename);
};

var read2 = function*() {
  return yield read(__filename);
};

suspend(function*() {
  var one = yield read1;
  var two = yield read2();
  var three = yield read(__filename);
  return [one, two, three];
})(function(e, res) {
  console.log(e,res);
});

当然,到这里,大家应该都明白如何让suspend兼容更多的数据类型,例如Promise、数组等。但更多的扩展,在这里就不再赘述。这里的suspend可以就说就是精简的co了。

yield的引入,让流程控制走上了一条康庄大道,不需要使用复杂的Promise、也不用使用难看的async。同时,从性能角度,yield可以通过V8的后续优化,性能进一步提升,目前来说yield的性能并不差5

yield的转换

yield的本质是一个语法糖,底层的实现方式便是CPS变换6。也就是说yield是可以用循环和递归重新实现的,根本用不着一定在V8层面实现。但笔者认为,纯Javascript实现的"yield"会造成大量的堆栈消耗,在性能上毫无优势可言。从性能上考虑,V8可以优化yield的编译,实现更高性能的转换。

关于CPS变换的细节,会在之后的文章中详细解说。

http://callbackhell.com/ ↩

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators ↩

https://github.com/JustinDrake/node-es6-examples#generators ↩

http://dailyjs.com/2013/05/31/suspend/ ↩

http://dailyjs.com/2013/10/17/yield/ ↩

http://en.wikipedia.org/wiki/Continuation-passing_style ↩

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/78131.html

相关文章

  • Javascript异步编程 - 函数编程 - Javascript核心

    摘要:不少第三方模块并没有做到异步调用,却装作支持回调,堆栈的风险就更大。我们可以编写一个高阶函数,让传入的函数顺序执行还是我们之前的例子看起来还是很不错的,简洁并且清晰,最终的代码量也没有增加。 原文: http://pij.robinqu.me/JavaScript_Core/Functional_JavaScript/Async_Programing_In_JavaScript....

    hlcc 评论0 收藏0
  • JavaScript异步编程解决方案笔记

    摘要:异步编程解决方案笔记最近读了朴灵老师的深入浅出中异步编程一章,并参考了一些有趣的文章。另外回调函数中的也失去了意义,这会使我们的程序必须依赖于副作用。 JavaScript 异步编程解决方案笔记 最近读了朴灵老师的《深入浅出NodeJS》中《异步编程》一章,并参考了一些有趣的文章。在此做个笔记,记录并巩固学到的知识。 JavaScript异步编程的两个核心难点 异步I/O、事件驱动使得...

    dmlllll 评论0 收藏0
  • js学习之异步处理

    摘要:学习开发,无论是前端开发还是都避免不了要接触异步编程这个问题就和其它大多数以多线程同步为主的编程语言不同的主要设计是单线程异步模型。由于异步编程可以实现非阻塞的调用效果,引入异步编程自然就是顺理成章的事情了。 学习js开发,无论是前端开发还是node.js,都避免不了要接触异步编程这个问题,就和其它大多数以多线程同步为主的编程语言不同,js的主要设计是单线程异步模型。正因为js天生的与...

    VioletJack 评论0 收藏0
  • JavaScript 异步

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。写一个符合规范并可配合使用的写一个符合规范并可配合使用的理解的工作原理采用回调函数来处理异步编程。 JavaScript怎么使用循环代替(异步)递归 问题描述 在开发过程中,遇到一个需求:在系统初始化时通过http获取一个第三方服务器端的列表,第三方服务器提供了一个接口,可通过...

    tuniutech 评论0 收藏0
  • Javascript中的Generator函数和yield关键字

    摘要:序在中,大家讨论的最多的就是异步编程的操作,如何避免回调的多次嵌套。今天所讲的和就是和异步编程有关,可以帮助我们把异步编程同步化。然而这样的方法依然需要依赖外在的库函数,于是中提出了和关键字。 序 在Javascript中,大家讨论的最多的就是异步编程的操作,如何避免回调的多次嵌套。异步操作的回调一旦嵌套很多,不仅代码会变的臃肿,还很容易出错。各种各样的异步编程解决方案也被不断提出,例...

    ZHAO_ 评论0 收藏0

发表评论

0条评论

yearsj

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<