Generator函數(shù)的語(yǔ)法和異步應(yīng)用

同步應(yīng)用

簡(jiǎn)介

基本概念

Generator函數(shù)式ES6提供的一種異步編程解決方案,語(yǔ)法行為和傳統(tǒng)函數(shù)完全不同.
語(yǔ)法上,首先可以把它理解成,Generator函數(shù)是一個(gè)狀態(tài)機(jī),封裝了多個(gè)內(nèi)部狀態(tài)
執(zhí)行Generator函數(shù)會(huì)返回一個(gè)遍歷器對(duì)象,也就是說(shuō),Generator函數(shù)除了狀態(tài)機(jī),還是一個(gè)遍歷器對(duì)象生成函數(shù).返回的遍歷器對(duì)象,可以依次遍歷Generator函數(shù)內(nèi)部的每一個(gè)狀態(tài)
形式上,Generator函數(shù)是一個(gè)普通函數(shù),但是有兩個(gè)特征.一是,function關(guān)鍵字與函數(shù)名之間有一個(gè)星號(hào);二是,函數(shù)體內(nèi)部使用yield(產(chǎn)出)表達(dá)式,定義不同的內(nèi)部狀態(tài).

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

調(diào)用Generator函數(shù),該函數(shù)并不執(zhí)行,返回的也不是函數(shù)運(yùn)行結(jié)果,而是一個(gè)指向內(nèi)部狀態(tài)的指針對(duì)象,也就是遍歷器對(duì)象.
下一步必須調(diào)用遍歷器對(duì)象的next方法,使得指針移向下一個(gè)狀態(tài).換言之,Generator函數(shù)是分段執(zhí)行的,yield表達(dá)式是暫停執(zhí)行的標(biāo)記,而next方法可以恢復(fù)執(zhí)行.
每次調(diào)用遍歷器對(duì)象的next方法,就會(huì)返回一個(gè)有著valuedone兩個(gè)屬性的對(duì)象.

yield表達(dá)式

yield表達(dá)式就是暫停標(biāo)志
遍歷器對(duì)象的next方法運(yùn)行邏輯:

  1. 遇到yield表達(dá)式,就暫停執(zhí)行后面的操作,并將緊跟著yield后面的那個(gè)表達(dá)式的值作為返回的對(duì)象的value屬性值
  2. 下一次調(diào)用next方法時(shí),再繼續(xù)向下執(zhí)行,知道遇到yield表達(dá)式
  3. 如果沒(méi)有遇到新的yield表達(dá)式,就一直運(yùn)行到函數(shù)結(jié)束,知道return語(yǔ)句為止.并將return語(yǔ)法后面的表達(dá)式的值,作為返回的對(duì)象的value
  4. 如果該函數(shù)沒(méi)有return語(yǔ)句 ,則返回的對(duì)象的value屬性值為undefined
    需要注意的是,yield表達(dá)式后面的表達(dá)式,只有調(diào)用next方法,內(nèi)部指針指向該語(yǔ)句時(shí)才會(huì)執(zhí)行,因此等于為JavaScript提供了手動(dòng)的"惰性求職"的語(yǔ)法功能
    Generator函數(shù)可以不用yield表達(dá)式,這時(shí)就變成了一個(gè)單純的暫緩執(zhí)行函數(shù)
    需要注意,yield表達(dá)式只能用在Generator函數(shù)里面.另外,yield表達(dá)式如果用在另一個(gè)表達(dá)式之中,必須放在圓括號(hào)里面.
    yield表達(dá)式用作函數(shù)參數(shù)或放在賦值表達(dá)式的右邊,可以不加括號(hào).
與Iterator接口的關(guān)系

任意一個(gè)對(duì)象的Symbol.iterator方法,等于該對(duì)象的遍歷器生成函數(shù),調(diào)用該函數(shù)會(huì)返回該對(duì)象的一個(gè)遍歷器對(duì)象
由于Generator函數(shù)就是遍歷器生成函數(shù),因此可以把Generator賦值給對(duì)象的Symbol.iterator屬性,從而使得該對(duì)象具有Iterator接口.
Generator函數(shù)執(zhí)行后,返回一個(gè)遍歷器對(duì)象,該對(duì)象本身也具有Symbol.iterator屬性,執(zhí)行后返回自身

next方法的參數(shù)

yield表達(dá)式本身沒(méi)有返回值,或者說(shuō)總是返回undefined.next方法可以帶一個(gè)參數(shù),該參數(shù)就會(huì)被當(dāng)做上一個(gè)yield表達(dá)式的返回值.
這個(gè)功能有很重要的語(yǔ)法意義.Generator函數(shù)從暫停狀態(tài)到恢復(fù)運(yùn)行,它的上下文狀態(tài)是不變的.通過(guò)next方法的參數(shù),就有辦法在Generator函數(shù)開(kāi)始運(yùn)行之后,繼續(xù)向函數(shù)內(nèi)部注入值.也就是說(shuō),可以再Generator函數(shù)運(yùn)行的不同階段,從外部想內(nèi)部注入不同的值,從而調(diào)整函數(shù)行為.

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

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

注意:由于next方法的參數(shù)表示上一個(gè)yield表達(dá)式的返回值,所以在第一次使用next方法時(shí),傳遞參數(shù)是無(wú)效的.只有從第二次使用next方法開(kāi)始,參數(shù)才是有效的
如果想要在第一次調(diào)用next方法時(shí),就能輸入值,可以在generator函數(shù)外面再包一層

function wrapper(generatorFunction) {
  return function (...args) {
    let generatorObject = generatorFunction(...args);
    generatorObject.next();
    return generatorObject;
  };
}

const wrapped = wrapper(function* () {
  console.log(`First input: ${yield}`);
  return 'DONE';
});

wrapped().next('hello!')
// First input: hello!

for...of循環(huán)

可以自動(dòng)遍歷Generator函數(shù)時(shí)生成的Iterator對(duì)象,且此時(shí)不再需要調(diào)用next方法.
這里需要注意:一旦next方法的返回對(duì)象的done屬性,for...of循環(huán)就會(huì)中止,且不包含該返回對(duì)象.
利用Generator函數(shù)和for...of循環(huán),實(shí)現(xiàn)斐波那契數(shù)列:

function* fibonacci() {
  let [prev, curr] = [0, 1];
  for (;;) {
    [prev, curr] = [curr, prev + curr];
    yield curr;
  }
}

for (let n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}

利用for...of循環(huán),可以寫出遍歷任意對(duì)象的方法.通過(guò)Generator函數(shù)為它加上遍歷器接口

function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

加上遍歷器接口的另一種寫法是,將Generator函數(shù)加到對(duì)象的Symbol.iterator屬性上

function* objectEntries() {
  let propKeys = Object.keys(this);

  for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

除了for...of循環(huán)以外,拓展運(yùn)算符(...)解構(gòu)賦值和Array.from方法內(nèi)部調(diào)用的,都是遍歷器接口.他們都可以講Generator函數(shù)返回的Iterator對(duì)象作為參數(shù)

Generator.prototype.throw

Generator函數(shù)返回的遍歷器對(duì)象,都有一個(gè)throw方法,可以在函數(shù)體外拋出錯(cuò)誤,然后在Generator函數(shù)體內(nèi)捕獲

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('內(nèi)部捕獲', e);
  }
};

var i = g();
i.next();

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕獲', e);
}
// 內(nèi)部捕獲 a
// 外部捕獲 b

第一個(gè)錯(cuò)誤被Generator函數(shù)體內(nèi)的catch語(yǔ)句捕獲.第二次拋出錯(cuò)誤,由于Generator函數(shù)內(nèi)部的catch語(yǔ)句已經(jīng)被執(zhí)行過(guò)了,不會(huì)再捕捉到這個(gè)錯(cuò)誤了,所以被函數(shù)體外的catch語(yǔ)句捕獲.
throw方法可以接受一個(gè)參數(shù),該參數(shù)會(huì)被catch語(yǔ)句接受,建議拋出Error對(duì)象的實(shí)例.
注意區(qū)分遍歷器對(duì)象的throw方法和全局的throw命令.

var g = function* () {
  while (true) {
    try {
      yield;
    } catch (e) {
      if (e != 'a') throw e;
      console.log('內(nèi)部捕獲', e);
    }
  }
};

var i = g();
i.next();

try {
  throw new Error('a');
  throw new Error('b');
} catch (e) {
  console.log('外部捕獲', e);
}
// 外部捕獲 [Error: a]

上面代碼之所以只捕獲了a,是因?yàn)楹瘮?shù)體外的catch語(yǔ)句塊捕獲了拋出的a錯(cuò)誤以后,就不會(huì)再繼續(xù)try代碼塊里面剩余的語(yǔ)句了.
如果Generator函數(shù)內(nèi)部沒(méi)有部署try...catch代碼塊,那么throw方法拋出的錯(cuò)誤,將被外部try...catch代碼塊捕獲.
如果內(nèi)外部都沒(méi)有部署try...catch代碼塊,那么程序?qū)?bào)錯(cuò)中斷執(zhí)行.
throw方法被捕獲以后,會(huì)附帶執(zhí)行下一條yield表達(dá)式,也即是說(shuō),會(huì)附帶執(zhí)行一次next方法.
只要Generator函數(shù)內(nèi)部部署了try...catch代碼塊,那么遍歷器的throw方法拋出的錯(cuò)誤,不影響下一次遍歷.
另外,throw命令和遍歷器中的throw方法是無(wú)關(guān)的,兩者互不影響.
Generator函數(shù)體外拋出的錯(cuò)誤可以在函數(shù)體內(nèi)捕獲;反過(guò)來(lái),Generator函數(shù)體內(nèi)拋出的錯(cuò)誤,也可以被函數(shù)體外的catch捕獲

function* foo() {
  var x = yield 3;
  var y = x.toUpperCase();
  yield y;
}

var it = foo();

it.next(); // { value:3, done:false }

try {
  it.next(42);
} catch (err) {
  console.log(err);
  //會(huì)有報(bào)錯(cuò)信息,數(shù)值沒(méi)有toUpperCase方法
}

一旦Generator執(zhí)行過(guò)程中拋出錯(cuò)誤,且沒(méi)有被內(nèi)部捕獲,就不會(huì)再執(zhí)行下去了.如果此后還調(diào)用next方法,將返回一個(gè)value屬性等于undefined,done屬性等于true的對(duì)象.

function* g() {
  yield 1;
  console.log('throwing an exception');
  throw new Error('generator broke!');
  yield 2;
  yield 3;
}

function log(generator) {
  var v;
  console.log('starting generator');
  try {
    v = generator.next();
    console.log('第一次運(yùn)行next方法', v);
  } catch (err) {
    console.log('捕捉錯(cuò)誤', v);
  }
  try {
    v = generator.next();
    console.log('第二次運(yùn)行next方法', v);
  } catch (err) {
    console.log('捕捉錯(cuò)誤', v);
  }
  try {
    v = generator.next();
    console.log('第三次運(yùn)行next方法', v);
  } catch (err) {
    console.log('捕捉錯(cuò)誤', v);
  }
  console.log('caller done');
}

log(g());
// starting generator
// 第一次運(yùn)行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉錯(cuò)誤 { value: 1, done: false }
// 第三次運(yùn)行next方法 { value: undefined, done: true }
// caller done

Generator.prototype.return

可以返回給定的值,并且終結(jié)遍歷Generator函數(shù).
如果return方法調(diào)用時(shí),不提供參數(shù),則返回值的value屬性為undefined.
如果Generator函數(shù)內(nèi)部有try...finally代碼塊,那么return方法會(huì)推遲到finally代碼塊執(zhí)行完再執(zhí)行

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }

next,throw,return的共同點(diǎn)

本質(zhì)上是同一件事,它們的作用都是讓Generator函數(shù)恢復(fù)執(zhí)行,并且使用各不同的語(yǔ)句替換yield表達(dá)式
next是將yield表達(dá)式替換成一個(gè)值

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}
// 相當(dāng)于將 let result = yield x + y
// 替換成 let result = 1;

throw是將yield表達(dá)式替換成一個(gè)throw語(yǔ)句

gen.throw(new Error('出錯(cuò)了')); // Uncaught Error: 出錯(cuò)了
// 相當(dāng)于將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯(cuò)了'));

return是將yield表達(dá)式替換成一個(gè)return`語(yǔ)句

gen.return(2); // Object {value: 2, done: true}
// 相當(dāng)于將 let result = yield x + y
// 替換成 let result = return 2;

yield*表達(dá)式

如果在Generator函數(shù)內(nèi)部,調(diào)用另一個(gè)Generator函數(shù),默認(rèn)情況下是沒(méi)有效果的.這里就需要用到yield*表達(dá)式,用來(lái)在一個(gè)Generator函數(shù)里面執(zhí)行另一個(gè)Generator函數(shù).

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"

yield*后面的Generator函數(shù)(沒(méi)有return語(yǔ)句時(shí)),等同于在Generator函數(shù)內(nèi)部,部署一個(gè)for...of循環(huán)
如果yield*后面跟著一個(gè)數(shù)組,由于數(shù)組原生支持遍歷器,因此就會(huì)遍歷數(shù)組成員

function* gen(){
  yield* ["a", "b", "c"];
}

gen().next() // { value:"a", done:false }

上面代碼中,如果yield命令后面如果不加星號(hào),返回的是整個(gè)數(shù)組,加了星號(hào)就表示返回的是數(shù)組的遍歷器對(duì)象.
如果被代理的Generator函數(shù)有return語(yǔ)句,那么就可以向代理它的Generator函數(shù)返回?cái)?shù)據(jù).
yield*命令可以很方便的取出嵌套數(shù)組的所有成員

function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for(let i=0; i < tree.length; i++) {
      yield* iterTree(tree[i]);
    }
  } else {
    yield tree;
  }
}

const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];

for(let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e

作為對(duì)象屬性的Generator函數(shù)

//簡(jiǎn)寫形式
let obj = {
  * myGeneratorMethod() {
    ···
  }
};
//等同于
let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

Generator函數(shù)的this

Generator函數(shù)總數(shù)返回一個(gè)遍歷器,ES6規(guī)定這個(gè)遍歷器是Generator函數(shù)的實(shí)例,也繼承了Generator函數(shù)的prototype對(duì)象上的方法.
Generator函數(shù)內(nèi)部在this對(duì)象上添加一個(gè)屬性,但是返回的遍歷器對(duì)象拿不到這個(gè)屬性.Generator函數(shù)也不能跟new命令一起用,會(huì)報(bào)錯(cuò).
讓Generator函數(shù)返回一個(gè)正常的對(duì)象實(shí)例,既可以用next方法,又可以獲得正常的this的方法:
首先,生成一個(gè)空對(duì)象,使用call方法綁定Generator函數(shù)內(nèi)部的this.這樣,構(gòu)造函數(shù)調(diào)用以后,這個(gè)空對(duì)象就是Generator函數(shù)的實(shí)例對(duì)象了.

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj = {};
var f = F.call(obj);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

obj.a // 1
obj.b // 2
obj.c // 3

上面代碼中,執(zhí)行的是遍歷器對(duì)象f,但是生成的對(duì)象實(shí)例是obj,將這個(gè)兩個(gè)對(duì)象統(tǒng)一的辦法:
將obj換成F.prototype

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var f = F.call(F.prototype);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

再將F改成構(gòu)造函數(shù),就可以對(duì)它執(zhí)行new命令了

function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

function F() {
  return gen.call(gen.prototype);
}

var f = new F();

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

含義

Generator和狀態(tài)機(jī)
//ES5
var ticking = true;
var clock = function() {
  if (ticking)
    console.log('Tick!');
  else
    console.log('Tock!');
  ticking = !ticking;
}
//ES6
var clock = function* () {
  while (true) {
    console.log('Tick!');
    yield;
    console.log('Tock!');
    yield;
  }
};

上面的Generator實(shí)現(xiàn)與ES5實(shí)現(xiàn)對(duì)比,可以看到少了用來(lái)保存狀態(tài)的外部變量,更符合函數(shù)式編程的思想.

Generator與協(xié)程

協(xié)程是一種程序運(yùn)行的方式,可以理解為"協(xié)作的線程"或"協(xié)作的函數(shù)".協(xié)程既可以用單線程實(shí)現(xiàn),也可以用多線程實(shí)現(xiàn).前者是一種特殊的子例程,后者是一種特殊的線程.

  1. 協(xié)程與子例程的差異
    可以并行執(zhí)行,交換執(zhí)行權(quán)的線程(或函數(shù)),就稱為協(xié)程.
    從實(shí)現(xiàn)上看,在內(nèi)存中,子例程只使用一個(gè)棧,而協(xié)程是同事存在多個(gè)棧,但只有一個(gè)棧是在運(yùn)行狀態(tài),也就是說(shuō),協(xié)程是以多占用內(nèi)存為代價(jià),實(shí)現(xiàn)多任務(wù)的并行.
  2. 協(xié)程與普通線程的差異
    普通的線程是搶先式的,到底哪個(gè)線程優(yōu)先得到資源,必須有運(yùn)行環(huán)境決定,但是協(xié)程是合作式的,執(zhí)行權(quán)有協(xié)程自己分配.
    Generator函數(shù)是ES6對(duì)于協(xié)程的實(shí)現(xiàn),但屬于不完全實(shí)現(xiàn).因?yàn)橹挥蠫enerator函數(shù)的調(diào)用者才能將程序的執(zhí)行權(quán)還給Generator函數(shù).如果是完全執(zhí)行的協(xié)程,任何函數(shù)都可以讓暫停的協(xié)程繼續(xù)之心.
Generator與上下文

Javascript執(zhí)行函數(shù)(或塊級(jí)代碼)的時(shí)候,會(huì)在當(dāng)前上下文環(huán)境的上層產(chǎn)生一個(gè)函數(shù)運(yùn)行的上下文,變成當(dāng)前的上下文,由此形成一個(gè)上下文環(huán)境的堆棧.這個(gè)堆棧是"后進(jìn)先出"的數(shù)據(jù)結(jié)構(gòu),最后產(chǎn)生的上下文環(huán)境首先執(zhí)行完成,退出堆棧,然后再執(zhí)行完成它下層的上下文,直至所有代碼執(zhí)行完成,堆棧清空
Generator函數(shù)不是這樣,它執(zhí)行產(chǎn)生的上下文環(huán)境,一旦遇到yield命令,就會(huì)暫時(shí)退出堆棧,但是并不小時(shí),里面的所有變量和對(duì)象會(huì)凍結(jié)在當(dāng)前狀態(tài).等到對(duì)它執(zhí)行next命令時(shí),這個(gè)上下文環(huán)境又會(huì)重新加入調(diào)用棧,凍結(jié)的變量和對(duì)象恢復(fù)執(zhí)行.

應(yīng)用

  1. 異步操作的同步化表達(dá)
    Ajax是典型的異步操作,通過(guò)Generator函數(shù)部署Ajax操作:
function* main() {
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
    console.log(resp.value);
}

function request(url) {
  makeAjaxCall(url, function(response){
    it.next(response);
    //回調(diào)成功之后再調(diào)用next方法,并傳入?yún)?shù)賦值給result
  });
}

var it = main();
it.next();

通過(guò)Generator函數(shù)逐行讀取文本文件

function* numbers() {
  let file = new FileReader("numbers.txt");
  try {
    while(!file.eof) {
      yield parseInt(file.readLine(), 10);
    }
  } finally {
    file.close();
  }
}
控制流管理
step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

Promise改寫

Promise.resolve(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(function (value4) {
    // Do something with value4
  }, function (error) {
    // Handle any error from step1 through step4
  })
  .done();

Generator函數(shù)改寫

function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

然后使用一個(gè)函數(shù)按次序自動(dòng)執(zhí)行所有步驟

scheduler(longRunningTask(initialValue));

function scheduler(task) {
  var taskObj = task.next(task.value);
  // 如果Generator函數(shù)未結(jié)束,就繼續(xù)調(diào)用
  if (!taskObj.done) {
    task.value = taskObj.value
    scheduler(task);
  }
}

這種做法只能用于所有步驟都是同步操作的情況,不能有異步操作的步驟.
for...of的本質(zhì)是一個(gè)while循環(huán).

部署Iterator接口

利用Generator函數(shù)可以在任意對(duì)象上部署Iterator接口

function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

// foo 3
// bar 7
作為數(shù)據(jù)結(jié)構(gòu)

可以看做一個(gè)數(shù)組結(jié)構(gòu),因?yàn)镚enerator函數(shù)可以返回一系列的值,這意味著它可以對(duì)任意表達(dá)式,提供類似數(shù)組的接口.

function* doStuff() {
  yield fs.readFile.bind(null, 'hello.txt');
  yield fs.readFile.bind(null, 'world.txt');
  yield fs.readFile.bind(null, 'and-such.txt');
}

for (task of doStuff()) {
  // task是一個(gè)函數(shù),可以像回調(diào)函數(shù)那樣使用它
}

異步應(yīng)用

傳統(tǒng)方法

ES6誕生以前,異步編程方法大概一下四種:

  • 回調(diào)函數(shù)
  • 事件監(jiān)聽(tīng)
  • 發(fā)布/訂閱
  • Promise對(duì)象

基本概念

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

如果依次讀取兩個(gè)以上的文件,就會(huì)出現(xiàn)多重嵌套.因?yàn)槎鄠€(gè)異步操作形成了搶耦合,只要有一個(gè)操作需要修改,她的上層回調(diào)函數(shù)和下層回調(diào)函數(shù)可能都要跟著修改.這種情況成為"回調(diào)函數(shù)地獄".
Promise就是為了解決這個(gè)問(wèn)題而提出的,它是一種新的寫法,允許將回調(diào)函數(shù)的嵌套,改為鏈?zhǔn)秸{(diào)用.采用Promise,連續(xù)讀取多個(gè)文件:

var 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最大問(wèn)題是代碼冗余,原來(lái)的任務(wù)被Promise包裝一下,不管什么操作都是一堆then,原來(lái)的語(yǔ)義變得不清楚

Generator函數(shù)

異步任務(wù)的封裝

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

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

上面代碼中,Generator函數(shù)封裝了一個(gè)異步操作,該操作先讀取一個(gè)遠(yuǎn)程接口,然后從JSON格式的數(shù)據(jù)解析信息

//執(zhí)行這段代碼
var g = gen();
var result = g.next();

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

上面代碼中,首先執(zhí)行Generator函數(shù),獲取遍歷器對(duì)象,然后使用next方法,執(zhí)行異步任務(wù)的第一階段.由于fetch模塊返回的是一個(gè)Promise對(duì)象,因此要用then方法調(diào)用下一個(gè)next方法.
可以看到,雖然Generator函數(shù)將異步操作表示的很簡(jiǎn)介,但是流程管理卻不方便.

Thunk函數(shù)

Thunk函數(shù)是自動(dòng)執(zhí)行Generator函數(shù)的一種方法

參數(shù)的求值策略
  1. "傳值調(diào)用"
    在進(jìn)入函數(shù)體之前,就計(jì)算參數(shù)表達(dá)式的值
  2. "傳名調(diào)用"
    直接將表達(dá)式傳入函數(shù)體,只有在用到的時(shí)候求值
Thunk函數(shù)的含義

編譯器的"傳名調(diào)用"實(shí)現(xiàn),往往是將參數(shù)放到一個(gè)臨時(shí)函數(shù)之中,再將這個(gè)臨時(shí)函數(shù)傳入函數(shù)體.這個(gè)臨時(shí)函數(shù)就叫做Thunk函數(shù).
這就是Thunk函數(shù)的定義,它是"傳名調(diào)用"的一種實(shí)現(xiàn)策略,用來(lái)替換某個(gè)表達(dá)式.

JavaScript語(yǔ)言的Thunk函數(shù)

JavaScript語(yǔ)言是傳值調(diào)用的e,它的Thunk函數(shù)含義有所不同.在JavaScript語(yǔ)言中,Thunk函數(shù)替換的不是表達(dá)式,而是多參數(shù)函數(shù),將其替換成一個(gè)只接受回調(diào)函數(shù)作為參數(shù)的單參數(shù)函數(shù).

// 正常版本的readFile(多參數(shù)版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(單參數(shù)版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

任何參數(shù),只要參數(shù)有回調(diào)函數(shù),就能寫成Thunk函數(shù)的形式.下面是一個(gè)簡(jiǎn)單的Thunk函數(shù)轉(zhuǎn)換器:

// ES5版本
var Thunk = function(fn){
  return function (){
    var args = Array.prototype.slice.call(arguments);
    return function (callback){
      args.push(callback);
      return fn.apply(this, args);
    }
  };
};

// ES6版本
const Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  };
};
Thunkify模塊

生產(chǎn)環(huán)境的轉(zhuǎn)換器,建議使用Thunkify模塊
使用方法:

var thunkify = require('thunkify');
var fs = require('fs');

var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
  // ...
});

Thunkify的源碼與上一節(jié)那個(gè)簡(jiǎn)單的轉(zhuǎn)換器非常像

function thunkify(fn) {
  return function() {
    var args = new Array(arguments.length);
    var ctx = this;

    for (var i = 0; i < args.length; ++i) {
      args[i] = arguments[i];
    }

    return function (done) {
      var called;

      args.push(function () {
        if (called) return;
        called = true;
        done.apply(null, arguments);
      });

      try {
        fn.apply(ctx, args);
      } catch (err) {
        done(err);
      }
    }
  }
};

主要多了一個(gè)檢查機(jī)制,變量called確?;卣{(diào)函數(shù)只運(yùn)行一次.這樣的設(shè)計(jì)與下文的Generator函數(shù)相關(guān).下面例子:

function f(a, b, callback){
  var sum = a + b;
  callback(sum);
  callback(sum);
}

var ft = thunkify(f);
var print = console.log.bind(console);
ft(1, 2)(print);
// 3

由于thunkify只允許回調(diào)函數(shù)執(zhí)行一次,所以只輸出一行結(jié)果.

Generator函數(shù)的流程管理

Generator函數(shù)可以自動(dòng)執(zhí)行

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

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

while(!res.done){
  console.log(res.value);
  res = g.next();
}

但是,這不適合異步操作.以讀取文件為例,下面的Generator函數(shù)封裝了兩個(gè)異步操作

var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);

var gen = function* (){
  var r1 = yield readFileThunk('/etc/fstab');
  console.log(r1.toString());
  var r2 = yield readFileThunk('/etc/shells');
  console.log(r2.toString());
};

上面代碼中,yield命令用于將程序的執(zhí)行權(quán)移出Generator函數(shù),那么就需要一種方法將執(zhí)行權(quán)再交還給Generator函數(shù)
這種方法就是Thunk函數(shù),因?yàn)樗梢栽诨卣{(diào)函數(shù)里,將執(zhí)行權(quán)交還給Generator函數(shù).

var g = gen();

var r1 = g.next();
//這里r1.value相當(dāng)于readFileThunk('/etc/fstab'),此時(shí)該方法還不會(huì)被執(zhí)行
//readFileThunk('/etc/fstab')(callback)
r1.value(function (err, data) {
  if (err) throw err;
  var r2 = g.next(data);
  //這里的r2.value已經(jīng)被賦值為Thunk函數(shù)名
  r2.value(function (err, data) {
    if (err) throw err;
    g.next(data);
  });
});

上面代碼中,變量g是Generator函數(shù)的內(nèi)部指針,表示目前執(zhí)行到哪一步.next方法負(fù)責(zé)將指針移動(dòng)到下一步,并返回該步的信息(value和done).
可以知道Generator函數(shù)的執(zhí)行結(jié)果,其實(shí)是將同一個(gè)回調(diào)函數(shù),反復(fù)傳入next方法的value屬性.這使得我們可以用遞歸來(lái)自動(dòng)完成這個(gè)過(guò)程.

Thunk函數(shù)的自動(dòng)流程管理

Thunk函數(shù)真正的威力,在于可以自動(dòng)執(zhí)行Generator函數(shù).下面就是一個(gè)基于Thunk函數(shù)的Generator執(zhí)行器.

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

  function next(err, data) {
    var result = gen.next(data);
    //此時(shí)result.value已經(jīng)被賦值為上一個(gè)Thunk函數(shù)名,next是一個(gè)回調(diào)函數(shù)
    if (result.done) return;
    result.value(next);
    //當(dāng)上一個(gè)Thunk函數(shù)執(zhí)行完成之后才會(huì)調(diào)用回調(diào)函數(shù),再繼續(xù)執(zhí)行下一個(gè)Thunk函數(shù)
  }

  next();
  //next方法從來(lái)都不會(huì)被傳入?yún)?shù)
}

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

run(g);

run函數(shù)就是一個(gè)Generator函數(shù)的自動(dòng)執(zhí)行器.內(nèi)部的next函數(shù)就是Thunk的回調(diào)函數(shù).next函數(shù)先將指針移到Generator函數(shù)的下一步(gen.next方法),然后判斷Generator函數(shù)是否結(jié)束(result.done屬性),如果沒(méi)結(jié)束,就將next函數(shù)再傳入Thunk函數(shù)(result.value
屬性),否則就直接退出
使用該執(zhí)行器的前提是每一個(gè)異步操作,都要是Thunk函數(shù),也就是說(shuō),跟在yield命令后面的必須是Thunk函數(shù).
Thunk函數(shù)并不是Generator函數(shù)自動(dòng)執(zhí)行的唯一方案.因?yàn)樽詣?dòng)執(zhí)行的關(guān)鍵是,必須有一種機(jī)制,自動(dòng)控制Generator函數(shù)的流程,接收和交換程序的執(zhí)行權(quán).回調(diào)函數(shù)可以做到這一點(diǎn),Promise對(duì)象也可以做到這一點(diǎn).

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在此處先列下本篇文章的主要內(nèi)容 簡(jiǎn)介 next方法的參數(shù) for...of循環(huán) Generator.prototy...
    醉生夢(mèng)死閱讀 1,486評(píng)論 3 8
  • 異步編程對(duì)JavaScript語(yǔ)言太重要。Javascript語(yǔ)言的執(zhí)行環(huán)境是“單線程”的,如果沒(méi)有異步編程,根本...
    呼呼哥閱讀 7,400評(píng)論 5 22
  • 簡(jiǎn)介 基本概念 Generator函數(shù)是ES6提供的一種異步編程解決方案,語(yǔ)法行為與傳統(tǒng)函數(shù)完全不同。本章詳細(xì)介紹...
    呼呼哥閱讀 1,135評(píng)論 0 4
  • 一、什么是生成器 Generator? 生成器對(duì)象是由一個(gè) Generator 函數(shù)返回的,并且她符合 可迭代協(xié)議...
    貴在隨心閱讀 1,146評(píng)論 0 3
  • 半夜起床,怒氣沖頂,看著婆婆的信息很想用力地反擊回去,有想到回過(guò)去除了撒氣什么作用也沒(méi)有,把編輯好的內(nèi)容給老公發(fā)了...
    游游游游上天的魚(yú)閱讀 280評(píng)論 0 0

友情鏈接更多精彩內(nèi)容