Javascript Generator - 函數式編程 - Javascript核心

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

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

  • 本文須要補充更多例子
  • 本文存在批註,但該網站的Markdown編輯器不支持,因此沒法正常展現,請到原文參考。

Javascript Generator

ES6中的Generator的引入,極大程度上改變了Javascript程序員對迭代器的見解,併爲解決callback hell1提供了新方法。git

Generator是一個與語言無關的特性,理論上它應該存在於全部Javascript引擎內,可是目前真正完整實現的,只有在node --harmony 下。因此後文全部的解釋,都以node環境舉例,須要的啓動參數爲node --harmony --use_strict程序員

V8中所實現的Generator和標準之中說的又有區別,這個能夠參考一下MDC的相關文檔2。並且,V8在寫做這篇文章時,並無實現Iterator。es6

用做迭代器

咱們以一個簡單的例子3開始:github

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

咱們能夠簡單的理解:app

  • 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-ofasync

說到這裏,大多數有經驗的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變換的細節,會在以後的文章中詳細解說。

相關文章
相關標籤/搜索