Generator函數

目錄

  • Generator語法node

    • yield
    • yield *表達式
    • next方法的參數
  • Generator爲何是異步編程解決方案
  • 異步應用git

    • Thunk函數
    • co模塊

JavaScript是單線程的,異步編程對於 JavaScript語言很是重要。若是沒有異步編程,根本無法用,得卡死不可。github

Generator語法

JavaScript開發者在代碼中幾乎廣泛依賴一個假定:一個函數一旦開始執行,就會運行結束,期間不會有其餘代碼打斷它並插入其中。可是ES6引入了一種新的函數類型,它並不符合這種運行到結束的特徵。這類新的函數被稱爲生成器。編程

更正一下上一篇文章對Iterator對象的翻譯,翻譯成中文應該爲迭代器。遍歷是一個動詞, 迭代器是名詞。

執行 Generator 函數返回一個迭代器對象。先來簡單回顧一下什麼是迭代器對象json

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        { 
            value: array[nextIndex++],
            done: false
        } 
        :
        {
            value: undefined,
            done: true
        };
    }
  };
}
const it = makeIterator(['a', 'b']);

it.next() 
// { value: "a", done: false }
it.next() 
// { value: "b", done: false }
it.next() 
// { value: undefined, done: true }

makeIterator函數就是用於生成迭代器對象的。
Generator 函數返回的遍歷其對象,能夠依次遍歷 Generator 函數內部的每個狀態。api

Generator 函數是一個普通函數,可是有兩個特徵。promise

    1. function 關鍵字與函數名以前有個星號
    1. 函數體內部使用yield表達式
function *helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

const hw = helloWorldGenerator();

上面的 定義了一個Generator 函數 helloWorldGenerator,它的內部有兩個yield表達式(Helloworld),即函數有三個狀態: Hello, worldreturn語句。數據結構

Generator 函數的調用方式和普通函數同樣,可是調用它並不執行,而是返回一個指向內部狀態的指針對象(Iterator對象多線程

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

上面一共調用了4次next方法異步

  1. Generator 函數 開始執行,知道遇到第一個 yield表達式,next()方法返回一個對象,它的done屬性就是當前yield表達式的值 Hello(這裏注意是yield表達值的值,並非yield表達式的返回值,yield表達式自己沒有返回值)。
  2. 下一次調用next 方法時,再繼續往下執行,直到遇到下一個 yield 表達式。
  3. 若是沒有再遇到新的yield表達式,就一直運行到函數結束,直到return語句爲止,並將return語句後面的表達式的值,做爲返回的對象的value屬性值。
  4. 若是該函數沒有return語句,則返回的對象的value屬性值爲undefined

yield

yield表達式是暫停標誌。

迭代器對象的next方法的運行邏輯:

  1. 遇到yield表達式,就暫停執行後面的操做,並將緊跟在yield後面的那個表達式的值,做爲返回對象的 value 屬性值。
  2. 下一次調用 next 方法,再繼續往下執行,直到遇到下一個yield表達式。
  3. 若是沒有再遇到新的 yield 表達式,就一直運行到函數結束,直到 return語句爲止,並將 return 語句後面的表達式的值,做爲返回值對象的value屬性值。
  4. 若是該函數沒有return語句,則返回的對象的value屬性值爲undefined

yield和return的區別

相同點:

都能返回緊跟在語句後面的那個表達式的值。

不一樣點:

  1. 每次遇到 yield,函數暫停執行,下一次再從該位置繼續日後執行,而return 語句不具有位置記憶的能力。
  2. 一個函數裏面只有執行一次 return 語句, 可是能夠執行屢次 yield 表達式
  3. 正常函數只能返回一個值,由於只能執行一次return ; Generator函數能夠返回一系列的值,由於有任意多個yield。(Generator 函數生成了一系列的值,也就是它爲何叫生成器的來歷)。

yield *

若是在 Generator函數內部,調用另外一個Generator函數,須要在前者的函數體內部,本身手動完成遍歷。

function *foo() {
  yield 'a';
  yield 'b';
}

function *bar() {
  yield 'x';
  // 手動遍歷 foo()
  for (let i of foo()) {
    console.log(i);
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// x
// a
// b
// y

foobar都是 Generator 函數,在bar裏面調用foo,就須要手動遍歷foo
ES6 提供了yield*表達式,做爲解決辦法,用來在一個 Generator 函數裏面執行另外一個 Generator 函數。

function *bar() {
  yield 'x';
  yield *foo();
  yield 'y';
}

// 等同於
function *bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同於
function *bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"

next方法的參數

next方法能夠帶有一個參數,該參數會被當作上一個yield表達式的返回值。yield表達式沒有返回值,或者說總返回 undefined

記住,next方法帶有的參數,會被當作上一個yield表達式的返回值,yield表達式沒有返回值。

本身默唸幾遍。而後看看下面代碼運行的輸出是什麼

function *foo(x) {
    const y = 2 * (yield (x + 1));
    const z = yield (y / 3);
    return (x + y + z);
}

const  a = foo(5);
console.log(a.next());
console.log(a.next());
console.log(a.next());

const b = foo(5);
console.log(b.next());
console.log(b.next(12));
console.log(b.next(13));

上面的運行結果是什麼

// { value: 6, done: false }
// { value: NaN, done: false }
// { value: NaN, done: true }

// { value: 6, done: false }
// { value: 8, done: false }
// { value: 42, done: true }

若是你真正理解了next方法帶有的參數,會被當作上一個yield表達式的返回值,yield表達式沒有返回值。這句話,相信這個題你必定能回答出來。

咱們來一塊兒看一下它的完整運行過程。

先看使用Generator函數生成的迭代器a:

  1. 第一次調用next方法,遇到 yield 中止,返回yield表達式的值,此時爲 5 + 1 = 6;
  2. 第二次調用next方法,遇到 yield 中止,返回yield表達式的值,因爲next方法沒有帶參數,上一個yield表達式返回值爲undefined, 致使y的值等於2*undefined即(NaN),除以 3 之後仍是NaN,所以返回對象的value屬性也等於NaN
  3. 第三次調用next方法,執行的是 return (x + y + z),此時x的值爲 5y的值爲 NaN, 因爲next方法沒有帶參數,上一個yield表達式返回值爲undefined,致使z爲 undefined,返回對象的 value屬性等於5 + NaN + undefined,即 NaN

在來看看使用Generator函數生成的迭代器b:

  1. 第一次調用next方法,遇到 yield 中止,返回yield表達式的值,此時爲 5 + 1 = 6;
  2. 第二次調用next方法,遇到 yield 中止,返回yield表達式的值,因爲next方法帶有參數12,因此上一個yield表達式返回值爲12, 所以y的值等於2*12即(24),除以 38,所以返回對象的value屬性爲8
  3. 第三次調用next方法,執行的是 return (x + y + z),此時x的值爲 5y的值爲 24, 因爲next方法沒有帶參數13,所以z爲13,返回對象的 value屬性等於5 + 24 + 13,即 42

這個功能有很重要的語法意義。Generator函數從暫停狀態到恢復運行,它的上下文狀態是不變的,經過next方法的參數,就有辦法在 Generator函數開始運行以後,繼續想函數體內注入值。

因爲 next方法的參數表示上一個 yield表達式的返回值,因此在第一次使用 next 方法時,傳遞參數是無效的。V8引擎直接忽略第一次使用 next方法時的參數,只有從第二次使用 next 方法開始,參數纔是有效的。從語義上講,第一個 next方法用來啓動迭代器對象,因此不用帶有參數。

迭代消息傳遞

Generator 函數 經過 yieldnext(...)實現了內建消息輸入輸出能力。

function *foo(x) {
    const y = x * (yield);
    return y;
}
// 啓動foo(...)
const it = foo(6);
it.next();
const res = it.next(7);
console.log(res.value);

首先,傳入6做爲參數x。而後調用 it.next(),這會啓動 *foo(..)

*foo(..) 內部,開始執行語句 const y = x ...,可是就遇到了一個yield表達式。它就會在這一點上暫停 *foo(..)(在賦值語句中間!),並在本質上要求調用代碼爲 yield 表達式提供一個結果值。

接下來,調用 it.next(7)`,這一句把值7傳回被暫停的 yield 表達式的結果。

因此,這時賦值語句實際上就是 const y = 6 * 7。如今,return y 返回值42做爲調用 it.next(7)的結果。

注意,這裏有一點很是重要,yieldnext(..)調用有一個不匹配。通常來講,須要的 next(..)調用要比 yield語句多一個,上面代碼片斷有一個yield和兩個next(..)調用。

爲何會有這個不匹配呢?
由於第一個 next()老是啓動一個生成器,並運行到第一個 yield處。不過,是第二個 next(...)調用完第一個被暫定的 yield表達式,第三個 next()調用完成第二個yield,以此類推。

Generator.prototype.throw()

Generator 函數返回的迭代器對象,都有一個throw方法,能夠在函數體外拋出錯誤,而後在 Generator 函數體內捕獲。

const g = function* () {
  try {
    yield;
  } catch (e) {
    console.log(e);
  }
};

const i = g();
i.next();
i.throw(new Error('出錯了!'));
// Error: 出錯了!(…)

Generator.prototype.return()

Generator 函數返回的迭代器對象,還有一個return方法,能夠返回給定的值,而且終結遍歷 Generator 函數。

function *gen() {
  yield 1;
  yield 2;
  yield 3;
}

const g = gen();

g.next();
// { value: 1, done: false }
g.return('foo');
// { value: "foo", done: true }
g.next();
// { value: undefined, done: true }

迭代器對象g調用return方法後,返回值的value屬性就是return方法的參數foo。而且,Generator 函數的遍歷就終止了,返回值的done屬性爲true,之後再調用next方法,done屬性老是返回true。

next()、throw()、return()

next()、throw()、return()這三個方法本質上是同一件事,能夠放在一塊兒理解。它們的做用都是讓 Generator 函數恢復執行,而且使用不一樣的語句替換yield表達式。

next()是將yield表達式替換成一個值。

const g = function* (x, y) {
  let result = yield x + y;
  return result;
};

const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}

gen.next(1); // Object {value: 1, done: true}
// 至關於將 let result = yield x + y
// 替換成 let result = 1;

throw()是將yield表達式替換成一個throw語句。

gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
// 至關於將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯了'));

return()是將yield表達式替換成一個return語句。

gen.return(2); // Object {value: 2, done: true}
// 至關於將 let result = yield x + y
// 替換成 let result = return 2;

多個迭代器

同一個 Generator函數的多個實例能夠同時運行,他們甚至能夠彼此交互

let z = 1;
function *foo() {
    const x = yield 2;
    z++;
    const y = yield (x * z);
    console.log(x, y, z);
}
const a = foo();
const b = foo();

let val1 = a.next().value;
console.log(val1);
// 2 <--  yield 2;

let val2 = b.next().value;
console.log(val2);
// 2 <--  yield 2;

val1 = a.next(val2 * 10).value;
console.log(val1);
// 40 <--  x: 20,z:2

val2 = b.next(val1 * 5).value;
console.log(val2);
//  600 <--  x: 200,z:3

a.next(val2 / 2);
// 20, 300, 3 <-- y: 300

b.next(val1 / 4);
// 200, 10, 3 <-- y: 10

咱們簡單梳理一下執行流程

  1. *foo()的兩個實例同時啓用,兩個next() 分別從yield 2 語句獲得2
  2. val2 * 10 也就是2 * 10,發送到第一個生成器實例 a, 由於x獲得的值20z1增長到2,而後 20 * 2經過 yield發出,將val1設置爲40
  3. val1 * 5 也就是 40 * 5,發送到第二個生成器實例 b,所以x獲得的值200z再從 2遞增到3,而後 200*3經過 yield 發出,將val2設置爲 600
  4. val2 / 2 也就是 600 / 2 發動到第一個生成器實例 a, 所以 y獲得值 300, 而後打印出 x y z 的值分別爲 20, 300, 3
  5. val1 / 4 也就是 40 / 4, 發送到第二個生成器實例 b, 所以 y獲得的值10, 而後打印出 x y z的值分別爲 200, 10, 3

for...of

使用for...of語句時不須要使用next方法。由於它能夠自動遍歷 Generator 函數運行時生成的 Iterator對象。

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  return 4;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

爲何只顯示3個yield表達式的值呢,

這是由於一旦 next方法的返回 對象的 done屬性爲 true,for...of 循環就中止,且不包含該返回對象,因此上面代碼的return語句返回的4,不包括在for...of循環之中。

咱們能夠直觀的來看一下 Generator 函數 foo 的遍歷過程

const it = foo();
console.log(it.next());
// { value: 1, done: false }
console.log(it.next());
// { value: 2, done: false }
console.log(it.next());
// { value: 3, done: false }
console.log(it.next());
// { value: 4, done: true }
console.log(it.next());
// { value: undefined, done: true }

能夠看到第一次 done返回爲true時,value4,即執行到最後一個 return 語句。因此 for...of 循環中不包含 4;

Generator爲何是異步編程解決方案

同步和異步

異步:一個任務不是連續完成的,能夠理解爲,先執行第一段,而後轉而執行其餘任務,等作好了準備,再回過頭執行第二段。
好比,你渴了要燒水(假如你的水壺能夠響),第一段任務是你要把水壺放到火上,這個時候你能夠先去幹其餘事情好比去看電視,過了一會,壺響了你聽到了執行第二段任務去倒水喝。這個就叫異步。

同步:連續的執行就叫同步。好比上面的例子,你把水壺放到火上以後,就一直等着水燒開,再去看電視,這就叫同步。

傳統解決異步的方法

回調函數

JavaScript語言對於異步編程的實現,就是回調函數。

回調函數自己並無問題,它的問題出如今多個回調函數嵌套。假定讀取A文件以後,再讀取B文件,

fs.readFile(fileA, 'utf-8', function (err, data) {
  fs.readFile(fileB, 'utf-8', function (err, data) {
    // ...
  });
});

上面這種狀況就稱爲"回調函數地獄"(callback hell)。代碼不是縱向發展,而是橫向發展,很快就會亂作一團,沒法管理。由於多個異步操做造成了強耦合,只要有一個操做須要更改,它的上層回調函數和下層回調函數,可能都要跟着修改。

Promise
// fs-readfile-promise模塊,它的做用就是返回一個 Promise 版本的readFile函數。
const readFile = require('fs-readfile-promise');

readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {
  console.log(data.toString());
})
.catch(function (err) {
  console.log(err);
});

Promise爲了解決 "回調函數地獄",它不是一種新語法,而是一種新寫法,把嵌套改爲了鏈式調用。並且代碼也很冗餘,一眼看上去一大堆then

協程

傳統的編程語言,早有異步編程的解決方案(實際上是多任務的解決方案)。其中有一種叫作"協程"(coroutine),意思是多個線程互相協做,完成異步任務。協程並非一個新的概念,其餘語言中很早就又了。

它的運行流程大體以下:

  • 第一步,協程A開始執行
  • 第一步,協程A執行到一半,進入暫停,執行權轉移到協程B。
  • 第三步,(一段時間後)協程A恢復執行
  • 上面流程的協程A,就是異步任務,由於它分紅兩段(或多段)執行。

協程既能夠用單線程實現,也能夠用多線程實現。

多個線程(單線程的狀況下,即多個函數)能夠並行執行,可是隻有一個線程(或函數)處於正在運行的狀態,其餘線程(或函數)都處於暫停態,線程(或函數)之間能夠交換執行權,也就是說,一個線程(或函數)執行到一半,能夠暫停執行,將執行權交給另外一個線程(或函數),等到稍後收回執行權的時候,再恢復執行。這種能夠並行執行、交換執行權的線程(或函數),就稱爲協程。

Generator 函數

協程的 Generator 函數實現

Generator 函數是協程在 ES6 的實現,Generator 函數是根據JavaScript單線程的特色實現的。
使用Generator 函數,徹底能夠將多個須要相互協做的任務寫成 Generator 函數 ,它們之間使用yield表達式交換控制權。

Generator 函數的上下文

JavaScript 代碼運行時,會產生一個全局的上下文環境(context,又稱運行環境),包含了當前全部的變量和對象。而後,執行函數(或塊級代碼)的時候,又會在當前上下文環境的上層,產生一個函數運行的上下文,變成當前(active)的上下文,由此造成一個上下文環境的堆棧。

這個堆棧是「後進先出」的數據結構,最後產生的上下文環境首先執行完成,退出堆棧,而後再執行完成它下層的上下文,直至全部代碼執行完成,堆棧清空。

Generator 函數不是這樣,它執行產生的上下文環境,一旦遇到yield命令,就會暫時退出堆棧,可是並不消失,裏面的全部變量和對象會凍結在當前狀態。等到對它執行next命令時,這個上下文環境又會從新加入調用棧,凍結的變量和對象恢復執行。

異步應用

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

能夠看到,雖然 Generator 函數將異步操做表示得很簡潔,可是流程管理卻不方便(即什麼時候執行第一階段、什麼時候執行第二階段)。

它不能自動執行,若是每次使用它都要本身手動寫一個執行函數的話,也使用起來其實反而更加麻煩了。相信你必定也想到了,咱們能夠實現一個自動執行的功能,自動控制 Generator函數的流程,接收和交換程序的執行權。

Thunk函數

JavaScript 語言的 Thunk 函數是將多參數函數,替換成一個只接受回調函數做爲參數的單參數函數。

任何函數,只要參數有回調函數,就能寫成 Thunk 函數的形式

const Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  };
};

使用上面的轉換器,生成fs.readFileThunk 函數。

const readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);

Thunk 函數用於 Generator 函數的自動流程管理

function run(fn) {
  var gen = fn();

  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
  }

  next();
}

function* g() {
  // ...
}

run(g);

co模塊

Generator 函數只要傳入co函數,就會自動執行。

co模塊的源碼
首先,co 函數接受 Generator 函數做爲參數,返回一個 Promise 對象。

function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
  });
}

在返回的 Promise 對象裏面,co 先檢查參數gen是否爲 Generator 函數。若是是,就執行該函數,獲得一個內部指針對象;若是不是就返回,並將 Promise 對象的狀態改成resolved。

function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
  });
}

接着,coGenerator 函數的內部指針對象的next方法,包裝成onFulfilled函數。這主要是爲了可以捕捉拋出的錯誤。

function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();
    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }
  });
}

最後,就是關鍵的next函數,它會反覆調用自身。

function next(ret) {
  if (ret.done) return resolve(ret.value);
  var value = toPromise.call(ctx, ret.value);
  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)
      + '"'
    )
  );
}

上面代碼中,next函數的內部代碼,一共只有四行命令。

第一行,檢查當前是否爲 Generator 函數的最後一步,若是是就返回。

第二行,確保每一步的返回值,是 Promise 對象。

第三行,使用then方法,爲返回值加上回調函數,而後經過onFulfilled函數再次調用next函數。

第四行,在參數不符合要求的狀況下(參數非 Thunk 函數和 Promise 對象),將 Promise 對象的狀態改成rejected,從而終止執行。

爲何 Thunk 函數和 co 模塊能夠自定執行 Generator函數?
Generator函數的自動執行須要一種機制,當異步操做有告終果,可以自動交回執行權。兩種方法能夠作到

  • 回調函數。將異步操做包裝成 Thunk函數,在回調函數裏面交回執行權
  • Promise 對象。將異步操做包裝成 Promise 對象,用then方法交回執行權。

co 模塊其實就是將兩種自動執行器(Thunk 函數和 Promise 對象),包裝成一個模塊。使用 co 的前提條件是,Generator 函數的yield命令後面,只能是 Thunk 函數或 Promise 對象。

總結

  • Generator(生成器) 函數 是ES6的一個新的函數類型,它並不像普通函數那樣老是運行到結束。Generator(生成器) 函數能夠在運行當中暫停,而且未來再從暫定的地方恢復運行
  • 能夠暫停執行(yield)恢復執行(next)Generator 函數能封裝異步任務的根本緣由。
  • 函數體內外的數據交換(next返回值的value,是向外輸出數據,next方法的參數,是向內輸入數據)和錯誤處理機制(Generator 函數內部還能夠部署錯誤處理代碼,捕獲函數體外拋出的錯誤。)是它能夠成爲異步編程的完整解決方案。
  • Generator 就是一個異步操做的容器。它的自動執行須要一種機制,當異步操做有告終果,可以自動交回執行權。因此須要自動化異步任務的流程管理。Thunk 函數是自動執行 Generator 函數的一種方法。co模塊也是用於 Generator 函數的自行執行。
相關文章
相關標籤/搜索