ES6 Generator函數(shù)的語法

2020-06-11 14:25 更新

1. 簡介

基本概念

Generator 函數(shù)是 ES6 提供的一種異步編程解決方案,語法行為與傳統(tǒng)函數(shù)完全不同。本章詳細(xì)介紹 Generator 函數(shù)的語法和 API,它的異步編程應(yīng)用請看《Generator 函數(shù)的異步應(yīng)用》一章。

Generator 函數(shù)有多種理解角度。語法上,首先可以把它理解成,Generator 函數(shù)是一個狀態(tài)機(jī),封裝了多個內(nèi)部狀態(tài)。

執(zhí)行 Generator 函數(shù)會返回一個遍歷器對象,也就是說,Generator 函數(shù)除了狀態(tài)機(jī),還是一個遍歷器對象生成函數(shù)。返回的遍歷器對象,可以依次遍歷 Generator 函數(shù)內(nèi)部的每一個狀態(tài)。

形式上,Generator函數(shù)是一個普通函數(shù),但是有兩個特征。一是, function 關(guān)鍵字與函數(shù)名之間有一個星號;二是,函數(shù)體內(nèi)部使用 yield 表達(dá)式,定義不同的內(nèi)部狀態(tài)( yield 在英語里的意思就是“產(chǎn)出”)。

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


var hw = helloWorldGenerator();

上面代碼定義了一個 Generator 函數(shù) helloWorldGenerator ,它內(nèi)部有兩個 yield 表達(dá)式( hello 和 world ),即該函數(shù)有三個狀態(tài):hello,world 和 return 語句(結(jié)束執(zhí)行)。

然后,Generator 函數(shù)的調(diào)用方法與普通函數(shù)一樣,也是在函數(shù)名后面加上一對圓括號。不同的是,調(diào)用 Generator 函數(shù)后,該函數(shù)并不執(zhí)行,返回的也不是函數(shù)運(yùn)行結(jié)果,而是一個指向內(nèi)部狀態(tài)的指針對象,也就是上一章介紹的遍歷器對象(Iterator Object)。

下一步,必須調(diào)用遍歷器對象的 next 方法,使得指針移向下一個狀態(tài)。也就是說,每次調(diào)用 next 方法,內(nèi)部指針就從函數(shù)頭部或上一次停下來的地方開始執(zhí)行,直到遇到下一個 yield 表達(dá)式(或 return 語句)為止。換言之,Generator 函數(shù)是分段執(zhí)行的, yield 表達(dá)式是暫停執(zhí)行的標(biāo)記,而 next 方法可以恢復(fù)執(zhí)行。

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


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


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


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

上面代碼一共調(diào)用了四次 next 方法。

第一次調(diào)用,Generator 函數(shù)開始執(zhí)行,直到遇到第一個 yield 表達(dá)式為止。 next 方法返回一個對象,它的 value 屬性就是當(dāng)前 yield 表達(dá)式的值 hello , done 屬性的值 false ,表示遍歷還沒有結(jié)束。

第二次調(diào)用,Generator 函數(shù)從上次 yield 表達(dá)式停下的地方,一直執(zhí)行到下一個 yield 表達(dá)式。 next 方法返回的對象的 value 屬性就是當(dāng)前 yield 表達(dá)式的值 world , done 屬性的值 false ,表示遍歷還沒有結(jié)束。

第三次調(diào)用,Generator 函數(shù)從上次 yield 表達(dá)式停下的地方,一直執(zhí)行到 return 語句(如果沒有 return 語句,就執(zhí)行到函數(shù)結(jié)束)。 next 方法返回的對象的 value 屬性,就是緊跟在 return 語句后面的表達(dá)式的值(如果沒有 return 語句,則 value 屬性的值為 undefined ), done 屬性的值 true ,表示遍歷已經(jīng)結(jié)束。

第四次調(diào)用,此時 Generator 函數(shù)已經(jīng)運(yùn)行完畢, next 方法返回對象的 value 屬性為 undefined , done 屬性為 true 。以后再調(diào)用 next 方法,返回的都是這個值。

總結(jié)一下,調(diào)用 Generator 函數(shù),返回一個遍歷器對象,代表 Generator 函數(shù)的內(nèi)部指針。以后,每次調(diào)用遍歷器對象的 next 方法,就會返回一個有著 value 和 done 兩個屬性的對象。 value 屬性表示當(dāng)前的內(nèi)部狀態(tài)的值,是 yield 表達(dá)式后面那個表達(dá)式的值; done 屬性是一個布爾值,表示是否遍歷結(jié)束。

ES6 沒有規(guī)定, function 關(guān)鍵字與函數(shù)名之間的星號,寫在哪個位置。這導(dǎo)致下面的寫法都能通過。

function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }

由于 Generator 函數(shù)仍然是普通函數(shù),所以一般的寫法是上面的第三種,即星號緊跟在 function 關(guān)鍵字后面。本書也采用這種寫法。

2. yield 表達(dá)式

由于 Generator 函數(shù)返回的遍歷器對象,只有調(diào)用 next 方法才會遍歷下一個內(nèi)部狀態(tài),所以其實(shí)提供了一種可以暫停執(zhí)行的函數(shù)。yield表達(dá)式就是暫停標(biāo)志

遍歷器對象的 next 方法的運(yùn)行邏輯如下。

(1)遇到 yield 表達(dá)式,就暫停執(zhí)行后面的操作,并將緊跟在 yield 后面的那個表達(dá)式的值,作為返回的對象的 value 屬性值。

(2)下一次調(diào)用 next 方法時,再繼續(xù)往下執(zhí)行,直到遇到下一個 yield 表達(dá)式。

(3)如果沒有再遇到新的 yield 表達(dá)式,就一直運(yùn)行到函數(shù)結(jié)束,直到 return 語句為止,并將 return 語句后面的表達(dá)式的值,作為返回的對象的 value 屬性值。

(4)如果該函數(shù)沒有 return 語句,則返回的對象的 value 屬性值為 undefined 。

需要注意的是, yield 表達(dá)式后面的表達(dá)式,只有當(dāng)調(diào)用 next 方法、內(nèi)部指針指向該語句時才會執(zhí)行,因此等于為 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能。

function* gen() {
  yield  123 + 456;
}

上面代碼中, yield 后面的表達(dá)式 123 + 456 ,不會立即求值,只會在 next 方法將指針移到這一句時,才會求值。

yield 表達(dá)式與 return 語句既有相似之處,也有區(qū)別。相似之處在于,都能返回緊跟在語句后面的那個表達(dá)式的值。區(qū)別在于每次遇到 yield ,函數(shù)暫停執(zhí)行,下一次再從該位置繼續(xù)向后執(zhí)行,而 return 語句不具備位置記憶的功能。一個函數(shù)里面,只能執(zhí)行一次(或者說一個) return 語句,但是可以執(zhí)行多次(或者說多個) yield 表達(dá)式。正常函數(shù)只能返回一個值,因?yàn)橹荒軋?zhí)行一次 return ;Generator 函數(shù)可以返回一系列的值,因?yàn)榭梢杂腥我舛鄠€ yield 。從另一個角度看,也可以說 Generator 生成了一系列的值,這也就是它的名稱的來歷(英語中,generator 這個詞是“生成器”的意思)。

Generator 函數(shù)可以不用 yield 表達(dá)式,這時就變成了一個單純的暫緩執(zhí)行函數(shù)。

function* f() {
  console.log('執(zhí)行了!')
}


var generator = f();


setTimeout(function () {
  generator.next()
}, 2000);

上面代碼中,函數(shù) f 如果是普通函數(shù),在為變量 generator 賦值時就會執(zhí)行。但是,函數(shù) f 是一個 Generator 函數(shù),就變成只有調(diào)用 next 方法時,函數(shù) f 才會執(zhí)行。

另外需要注意, yield 表達(dá)式只能用在 Generator 函數(shù)里面,用在其他地方都會報錯。

(function (){
  yield 1;
})()
// SyntaxError: Unexpected number

上面代碼在一個普通函數(shù)中使用 yield 表達(dá)式,結(jié)果產(chǎn)生一個句法錯誤。

下面是另外一個例子。

var arr = [1, [[2, 3], 4], [5, 6]];


var flat = function* (a) {
  a.forEach(function (item) {
    if (typeof item !== 'number') {
      yield* flat(item);
    } else {
      yield item;
    }
  });
};


for (var f of flat(arr)){
  console.log(f);
}

上面代碼也會產(chǎn)生句法錯誤,因?yàn)?forEach 方法的參數(shù)是一個普通函數(shù),但是在里面使用了 yield 表達(dá)式(這個函數(shù)里面還使用了 yield* 表達(dá)式,詳細(xì)介紹見后文)。一種修改方法是改用 for 循環(huán)。

var arr = [1, [[2, 3], 4], [5, 6]];


var flat = function* (a) {
  var length = a.length;
  for (var i = 0; i < length; i++) {
    var item = a[i];
    if (typeof item !== 'number') {
      yield* flat(item);
    } else {
      yield item;
    }
  }
};


for (var f of flat(arr)) {
  console.log(f);
}
// 1, 2, 3, 4, 5, 6

另外, yield 表達(dá)式如果用在另一個表達(dá)式之中,必須放在圓括號里面。

function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError


  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}

yield 表達(dá)式用作函數(shù)參數(shù)或放在賦值表達(dá)式的右邊,可以不加括號。

function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}

與 Iterator 接口的關(guān)系

上一章說過,任意一個對象的 Symbol.iterator 方法,等于該對象的遍歷器生成函數(shù),調(diào)用該函數(shù)會返回該對象的一個遍歷器對象。

由于 Generator 函數(shù)就是遍歷器生成函數(shù),因此可以把Generator 賦值給對象的 Symbol.iterator 屬性,從而使得該對象具有Iterator 接口。

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};


[...myIterable] // [1, 2, 3]

上面代碼中,Generator函數(shù)賦值給 Symbol.iterator 屬性,從而使得 myIterable對象具有了 Iterator 接口,可以被 ... 運(yùn)算符遍歷了。

Generator 函數(shù)執(zhí)行后,返回一個遍歷器對象。該對象本身也具有 Symbol.iterator 屬性,執(zhí)行后返回自身。

function* gen(){
  // some code
}


var g = gen();


g[Symbol.iterator]() === g
// true

上面代碼中, gen 是一個 Generator 函數(shù),調(diào)用它會生成一個遍歷器對象 g 。它的 Symbol.iterator 屬性,也是一個遍歷器對象生成函數(shù),執(zhí)行后返回它自己。

2. next 方法的參數(shù)

yield表達(dá)式本身沒有返回值,或者說總是返回 undefined 。 next 方法可以帶一個參數(shù),該參數(shù)就會被當(dāng)作上一個 yield 表達(dá)式的返回值。

function* f() {
  for(var i = 0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}


var g = f();


g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }

上面代碼先定義了一個可以無限運(yùn)行的Generator函數(shù) f ,如果 next 方法沒有參數(shù),每次運(yùn)行到 yield 表達(dá)式,變量 reset 的值總是 undefined 。當(dāng) next 方法帶一個參數(shù) true 時,變量 reset 就被重置為這個參數(shù)(即 true ),因此 i 會等于 -1 ,下一輪循環(huán)就會從 -1 開始遞增。

這個功能有很重要的語法意義。Generator 函數(shù)從暫停狀態(tài)到恢復(fù)運(yùn)行,它的上下文狀態(tài)(context)是不變的。通過 next 方法的參數(shù),就有辦法在 Generator 函數(shù)開始運(yùn)行之后,繼續(xù)向函數(shù)體內(nèi)部注入值。也就是說,可以在 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 }

上面代碼中,第二次運(yùn)行 next 方法的時候不帶參數(shù),導(dǎo)致 y 的值等于 2 * undefined (即 NaN ),除以 3 以后還是 NaN ,因此返回對象的 value 屬性也等于 NaN 。第三次運(yùn)行 Next 方法的時候不帶參數(shù),所以 z 等于 undefined ,返回對象的 value 屬性等于 5 + NaN + undefined ,即 NaN 。

如果向 next 方法提供參數(shù),返回結(jié)果就完全不一樣了。上面代碼第一次調(diào)用 b 的 next 方法時,返回 x+1 的值 6 ;第二次調(diào)用 next 方法,將上一次 yield 表達(dá)式的值設(shè)為 12 ,因此 y 等于 24 ,返回 y / 3 的值 8 ;第三次調(diào)用 next 方法,將上一次 yield 表達(dá)式的值設(shè)為 13 ,因此 z 等于 13 ,這時 x 等于 5 , y 等于 24 ,所以 return 語句的值等于 42 。

注意,由于 next 方法的參數(shù)表示上一個 yield 表達(dá)式的返回值,所以在第一次使用 next 方法時,傳遞參數(shù)是無效的。V8 引擎直接忽略第一次使用 next 方法時的參數(shù),只有從第二次使用 next 方法開始,參數(shù)才是有效的。從語義上講,第一個 next 方法用來啟動遍歷器對象,所以不用帶有參數(shù)。

再看一個通過 next 方法的參數(shù),向 Generator 函數(shù)內(nèi)部輸入值的例子。

function* dataConsumer() {
  console.log('Started');
  console.log( 1. ${yield} );
  console.log( 2. ${yield} );
  return 'result';
}


let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b

上面代碼是一個很直觀的例子,每次通過 next 方法向 Generator 函數(shù)輸入值,然后打印出來。

如果想要第一次調(diào)用 next 方法時,就能夠輸入值,可以在 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!

上面代碼中,Generator 函數(shù)如果不用 wrapper 先包一層,是無法第一次調(diào)用 next 方法,就輸入?yún)?shù)的。

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

for...of循環(huán)可以自動遍歷 Generator 函數(shù)運(yùn)行時生成的Iterator對象,且此時不再需要調(diào)用 next 方法。

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


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

上面代碼使用 for...of 循環(huán),依次顯示 5 個 yield 表達(dá)式的值。這里需要注意,一旦 next 方法的返回對象的 done 屬性為 true , for...of 循環(huán)就會中止,且不包含該返回對象,所以上面代碼的 return 語句返回的 6 ,不包括在 for...of 循環(huán)之中。

下面是一個利用 Generator 函數(shù)和 for...of 循環(huán),實(shí)現(xiàn)斐波那契數(shù)列的例子。

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


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

從上面代碼可見,使用 for...of 語句時不需要使用 next 方法。

利用 for...of 循環(huán),可以寫出遍歷任意對象(object)的方法。原生的 JavaScript 對象沒有遍歷接口,無法使用 for...of 循環(huán),通過 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

上面代碼中,對象 jane 原生不具備 Iterator 接口,無法用 for...of 遍歷。這時,我們通過 Generator 函數(shù) objectEntries 為它加上遍歷器接口,就可以用 for...of 遍歷了。加上遍歷器接口的另一種寫法是,將 Generator 函數(shù)加到對象的 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)以外,擴(kuò)展運(yùn)算符( ... )、解構(gòu)賦值和 Array.from 方法內(nèi)部調(diào)用的,都是遍歷器接口。這意味著,它們都可以將 Generator 函數(shù)返回的 Iterator 對象,作為參數(shù)。

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


// 擴(kuò)展運(yùn)算符
[...numbers()] // [1, 2]


// Array.from 方法
Array.from(numbers()) // [1, 2]


// 解構(gòu)賦值
let [x, y] = numbers();
x // 1
y // 2


// for...of 循環(huán)
for (let n of numbers()) {
  console.log(n)
}
// 1
// 2

4. Generator.prototype.throw()

Generator函數(shù)返回的遍歷器對象,都有一個 throw方法,可以在函數(shù)體外拋出錯誤,然后在 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

上面代碼中,遍歷器對象 i 連續(xù)拋出兩個錯誤。第一個錯誤被 Generator 函數(shù)體內(nèi)的 catch 語句捕獲。 i 第二次拋出錯誤,由于 Generator 函數(shù)內(nèi)部的 catch 語句已經(jīng)執(zhí)行過了,不會再捕捉到這個錯誤了,所以這個錯誤就被拋出了 Generator 函數(shù)體,被函數(shù)體外的 catch 語句捕獲。

throw 方法可以接受一個參數(shù),該參數(shù)會被 catch 語句接收,建議拋出 Error 對象的實(shí)例。

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


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

注意,不要混淆遍歷器對象的 throw 方法和全局的 throw 命令。上面代碼的錯誤,是用遍歷器對象的 throw 方法拋出的,而不是用 throw 命令拋出的。后者只能被函數(shù)體外的 catch 語句捕獲。

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 語句塊,捕獲了拋出的 a 錯誤以后,就不會再繼續(xù) try 代碼塊里面剩余的語句了。

如果 Generator 函數(shù)內(nèi)部沒有部署 try...catch 代碼塊,那么 throw 方法拋出的錯誤,將被外部 try...catch 代碼塊捕獲。

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


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


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

上面代碼中,Generator 函數(shù) g 內(nèi)部沒有部署 try...catch 代碼塊,所以拋出的錯誤直接被外部 catch 代碼塊捕獲。

如果 Generator 函數(shù)內(nèi)部和外部,都沒有部署 try...catch 代碼塊,那么程序?qū)箦e,直接中斷執(zhí)行。

var gen = function* gen(){
  yield console.log('hello');
  yield console.log('world');
}


var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined

上面代碼中, g.throw 拋出錯誤以后,沒有任何 try...catch 代碼塊可以捕獲這個錯誤,導(dǎo)致程序報錯,中斷執(zhí)行。

throw 方法拋出的錯誤要被內(nèi)部捕獲,前提是必須至少執(zhí)行過一次 next 方法。

function* gen() {
  try {
    yield 1;
  } catch (e) {
    console.log('內(nèi)部捕獲');
  }
}


var g = gen();
g.throw(1);
// Uncaught 1

上面代碼中, g.throw(1) 執(zhí)行時, next 方法一次都沒有執(zhí)行過。這時,拋出的錯誤不會被內(nèi)部捕獲,而是直接在外部拋出,導(dǎo)致程序出錯。這種行為其實(shí)很好理解,因?yàn)榈谝淮螆?zhí)行 next 方法,等同于啟動執(zhí)行 Generator 函數(shù)的內(nèi)部代碼,否則 Generator 函數(shù)還沒有開始執(zhí)行,這時 throw 方法拋錯只可能拋出在函數(shù)外部。

throw 方法被捕獲以后,會附帶執(zhí)行下一條 yield 表達(dá)式。也就是說,會附帶執(zhí)行一次 next 方法。

var gen = function* gen(){
  try {
    yield console.log('a');
  } catch (e) {
    // ...
  }
  yield console.log('b');
  yield console.log('c');
}


var g = gen();
g.next() // a
g.throw() // b
g.next() // c

上面代碼中, g.throw 方法被捕獲以后,自動執(zhí)行了一次 next 方法,所以會打印 b 。另外,也可以看到,只要 Generator 函數(shù)內(nèi)部部署了 try...catch 代碼塊,那么遍歷器的 throw 方法拋出的錯誤,不影響下一次遍歷。

另外, throw 命令與 g.throw 方法是無關(guān)的,兩者互不影響。

var gen = function* gen(){
  yield console.log('hello');
  yield console.log('world');
}


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


try {
  throw new Error();
} catch (e) {
  g.next();
}
// hello
// world

上面代碼中, throw 命令拋出的錯誤不會影響到遍歷器的狀態(tài),所以兩次執(zhí)行 next 方法,都進(jìn)行了正確的操作。

這種函數(shù)體內(nèi)捕獲錯誤的機(jī)制,大大方便了對錯誤的處理。多個 yield 表達(dá)式,可以只用一個 try...catch 代碼塊來捕獲錯誤。如果使用回調(diào)函數(shù)的寫法,想要捕獲多個錯誤,就不得不為每個函數(shù)內(nèi)部寫一個錯誤處理語句,現(xiàn)在只在 Generator 函數(shù)內(nèi)部寫一次 catch 語句就可以了。

Generator 函數(shù)體外拋出的錯誤,可以在函數(shù)體內(nèi)捕獲;反過來,Generator 函數(shù)體內(nèi)拋出的錯誤,也可以被函數(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);
}

上面代碼中,第二個 next 方法向函數(shù)體內(nèi)傳入一個參數(shù) 42,數(shù)值是沒有 toUpperCase 方法的,所以會拋出一個 TypeError 錯誤,被函數(shù)體外的 catch 捕獲。

一旦 Generator 執(zhí)行過程中拋出錯誤,且沒有被內(nèi)部捕獲,就不會再執(zhí)行下去了。如果此后還調(diào)用 next 方法,將返回一個 value 屬性等于 undefined 、 done 屬性等于 true 的對象,即 JavaScript 引擎認(rèn)為這個 Generator 已經(jīng)運(yùn)行結(jié)束了。

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('捕捉錯誤', v);
  }
  try {
    v = generator.next();
    console.log('第二次運(yùn)行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  try {
    v = generator.next();
    console.log('第三次運(yùn)行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  console.log('caller done');
}


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

上面代碼一共三次運(yùn)行 next 方法,第二次運(yùn)行的時候會拋出錯誤,然后第三次運(yùn)行的時候,Generator 函數(shù)就已經(jīng)結(jié)束了,不再執(zhí)行下去了。

5. Generator.prototype.return()

Generator函數(shù)返回的遍歷器對象,還有一個 return方法,可以返回給定的值,并且終結(jié)遍歷 Generator 函數(shù)。

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


var g = gen();


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

上面代碼中,遍歷器對象 g 調(diào)用 return 方法后,返回值的 value 屬性就是 return 方法的參數(shù) foo 。并且,Generator 函數(shù)的遍歷就終止了,返回值的 done 屬性為 true ,以后再調(diào)用 next 方法, done 屬性總是返回 true 。

如果 return 方法調(diào)用時,不提供參數(shù),則返回值的 value 屬性為 undefined 。

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


var g = gen();


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

如果 Generator 函數(shù)內(nèi)部有 try...finally 代碼塊,且正在執(zhí)行 try 代碼塊,那么 return 方法會導(dǎo)致立刻進(jìn)入 finally 代碼塊,執(zhí)行完以后,整個函數(shù)才會結(jié)束。

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 }

上面代碼中,調(diào)用 return() 方法后,就開始執(zhí)行 finally 代碼塊,不執(zhí)行 try 里面剩下的代碼了,然后等到 finally 代碼塊執(zhí)行完,再返回 return() 方法指定的返回值。

6. next()、throw()、return() 的共同點(diǎn)

next() 、throw() 、 return() 這三個方法本質(zhì)上是同一件事,可以放在一起理解。它們的作用都是讓 Generator 函數(shù)恢復(fù)執(zhí)行,并且使用不同的語句替換 yield 表達(dá)式。

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

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;

上面代碼中,第二個 next(1) 方法就相當(dāng)于將 yield 表達(dá)式替換成一個值 1 。如果 next 方法沒有參數(shù),就相當(dāng)于替換成 undefined 。

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

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

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

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

7. yield* 表達(dá)式

如果在 Generator 函數(shù)內(nèi)部,調(diào)用另一個 Generator 函數(shù)。需要在前者的函數(shù)體內(nèi)部,自己手動完成遍歷。

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

上面代碼中, foo 和 bar 都是 Generator 函數(shù),在 bar 里面調(diào)用 foo ,就需要手動遍歷 foo 。如果有多個 Generator 函數(shù)嵌套,寫起來就非常麻煩。

ES6 提供了 yield* 表達(dá)式,作為解決辦法,用來在一個 Generator 函數(shù)里面執(zhí)行另一個 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"

再來看一個對比的例子。

function* inner() {
  yield 'hello!';
}


function* outer1() {
  yield 'open';
  yield inner();
  yield 'close';
}


var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一個遍歷器對象
gen.next().value // "close"


function* outer2() {
  yield 'open'
  yield* inner()
  yield 'close'
}


var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"

上面例子中, outer2 使用了 yield* , outer1 沒使用。結(jié)果就是, outer1 返回一個遍歷器對象, outer2 返回該遍歷器對象的內(nèi)部值。

從語法角度看,如果 yield 表達(dá)式后面跟的是一個遍歷器對象,需要在 yield 表達(dá)式后面加上星號,表明它返回的是一個遍歷器對象。這被稱為 yield* 表達(dá)式。

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


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


for(let value of delegatingIterator) {
  console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."

上面代碼中, delegatingIterator 是代理者, delegatedIterator 是被代理者。由于 yield* delegatedIterator 語句得到的值,是一個遍歷器,所以要用星號表示。運(yùn)行結(jié)果就是使用一個遍歷器,遍歷了多個 Generator 函數(shù),有遞歸的效果。

yield* 后面的 Generator 函數(shù)(沒有 return 語句時),等同于在 Generator 函數(shù)內(nèi)部,部署一個 for...of 循環(huán)。

function* concat(iter1, iter2) {
  yield* iter1;
  yield* iter2;
}


// 等同于


function* concat(iter1, iter2) {
  for (var value of iter1) {
    yield value;
  }
  for (var value of iter2) {
    yield value;
  }
}

上面代碼說明, yield 后面的 Generator 函數(shù)(沒有 return 語句時),不過是 for...of 的一種簡寫形式,完全可以用后者替代前者。反之,在有 return 語句時,則需要用 var value = yield iterator 的形式獲取 return 語句的值。

如果 yield* 后面跟著一個數(shù)組,由于數(shù)組原生支持遍歷器,因此就會遍歷數(shù)組成員。

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


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

上面代碼中, yield 命令后面如果不加星號,返回的是整個數(shù)組,加了星號就表示返回的是數(shù)組的遍歷器對象。

實(shí)際上,任何數(shù)據(jù)結(jié)構(gòu)只要有 Iterator 接口,就可以被 yield* 遍歷。

let read = (function* () {
  yield 'hello';
  yield* 'hello';
})();


read.next().value // "hello"
read.next().value // "h"

上面代碼中, yield 表達(dá)式返回整個字符串, yield 語句返回單個字符。因?yàn)樽址哂?Iterator 接口,所以被 yield 遍歷。

如果被代理的 Generator 函數(shù)有 return 語句,那么就可以向代理它的 Generator 函數(shù)返回數(shù)據(jù)。

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


function* bar() {
  yield 1;
  var v = yield* foo();
  console.log("v: " + v);
  yield 4;
}


var it = bar();


it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}

上面代碼在第四次調(diào)用 next 方法的時候,屏幕上會有輸出,這是因?yàn)楹瘮?shù) foo 的 return 語句,向函數(shù) bar 提供了返回值。

再看一個例子。

function* genFuncWithReturn() {
  yield 'a';
  yield 'b';
  return 'The result';
}
function* logReturned(genObj) {
  let result = yield* genObj;
  console.log(result);
}


[...logReturned(genFuncWithReturn())]
// The result
// 值為 [ 'a', 'b' ]

上面代碼中,存在兩次遍歷。第一次是擴(kuò)展運(yùn)算符遍歷函數(shù) logReturned 返回的遍歷器對象,第二次是 yield* 語句遍歷函數(shù) genFuncWithReturn 返回的遍歷器對象。這兩次遍歷的效果是疊加的,最終表現(xiàn)為擴(kuò)展運(yùn)算符遍歷函數(shù) genFuncWithReturn 返回的遍歷器對象。所以,最后的數(shù)據(jù)表達(dá)式得到的值等于 [ 'a', 'b' ] 。但是,函數(shù) genFuncWithReturn 的 return 語句的返回值 The result ,會返回給函數(shù) logReturned 內(nèi)部的 result 變量,因此會有終端輸出。

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

由于擴(kuò)展運(yùn)算符 ... 默認(rèn)調(diào)用 Iterator 接口,所以上面這個函數(shù)也可以用于嵌套數(shù)組的平鋪。

[...iterTree(tree)] // ["a", "b", "c", "d", "e"]

下面是一個稍微復(fù)雜的例子,使用 yield* 語句遍歷完全二叉樹。

// 下面是二叉樹的構(gòu)造函數(shù),
// 三個參數(shù)分別是左樹、當(dāng)前節(jié)點(diǎn)和右樹
function Tree(left, label, right) {
  this.left = left;
  this.label = label;
  this.right = right;
}


// 下面是中序(inorder)遍歷函數(shù)。
// 由于返回的是一個遍歷器,所以要用generator函數(shù)。
// 函數(shù)體內(nèi)采用遞歸算法,所以左樹和右樹要用yield*遍歷
function* inorder(t) {
  if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
  }
}


// 下面生成二叉樹
function make(array) {
  // 判斷是否為葉節(jié)點(diǎn)
  if (array.length == 1) return new Tree(null, array[0], null);
  return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);


// 遍歷二叉樹
var result = [];
for (let node of inorder(tree)) {
  result.push(node);
}


result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']

8. 作為對象屬性的 Generator 函數(shù)

如果一個對象的屬性是 Generator 函數(shù),可以簡寫成下面的形式。

let obj = {
  * myGeneratorMethod() {
    ···
  }
};

上面代碼中, myGeneratorMethod 屬性前面有一個星號,表示這個屬性是一個 Generator 函數(shù)。

它的完整形式如下,與上面的寫法是等價的。

let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

9. Generator 函數(shù)的 this

Generator 函數(shù)總是返回一個遍歷器,ES6 規(guī)定這個遍歷器是 Generator 函數(shù)的實(shí)例,也繼承了 Generator函數(shù)的 prototype對象上的方法。

function* g() {}


g.prototype.hello = function () {
  return 'hi!';
};


let obj = g();


obj instanceof g // true
obj.hello() // 'hi!'

上面代碼表明,Generator 函數(shù) g 返回的遍歷器 obj ,是 g 的實(shí)例,而且繼承了 g.prototype 。但是,如果把 g 當(dāng)作普通的構(gòu)造函數(shù),并不會生效,因?yàn)?g 返回的總是遍歷器對象,而不是 this 對象。

function* g() {
  this.a = 11;
}


let obj = g();
obj.next();
obj.a // undefined

上面代碼中,Generator 函數(shù) g 在 this 對象上面添加了一個屬性 a ,但是 obj 對象拿不到這個屬性。

Generator 函數(shù)也不能跟 new 命令一起用,會報錯。

function* F() {
  yield this.x = 2;
  yield this.y = 3;
}


new F()
// TypeError: F is not a constructor

上面代碼中, new 命令跟構(gòu)造函數(shù) F 一起使用,結(jié)果報錯,因?yàn)?F 不是構(gòu)造函數(shù)。

那么,有沒有辦法讓 Generator 函數(shù)返回一個正常的對象實(shí)例,既可以用 next 方法,又可以獲得正常的 this ?

下面是一個變通方法。首先,生成一個空對象,使用 call 方法綁定 Generator 函數(shù)內(nèi)部的 this 。這樣,構(gòu)造函數(shù)調(diào)用以后,這個空對象就是 Generator 函數(shù)的實(shí)例對象了。

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

上面代碼中,首先是 F 內(nèi)部的 this 對象綁定 obj 對象,然后調(diào)用它,返回一個 Iterator 對象。這個對象執(zhí)行三次 next 方法(因?yàn)?F 內(nèi)部有兩個 yield 表達(dá)式),完成 F 內(nèi)部所有代碼的運(yùn)行。這時,所有內(nèi)部屬性都綁定在 obj 對象上了,因此 obj 對象也就成了 F 的實(shí)例。

上面代碼中,執(zhí)行的是遍歷器對象 f ,但是生成的對象實(shí)例是 obj ,有沒有辦法將這兩個對象統(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ù),就可以對它執(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

10. 含義

Generator 與狀態(tài)機(jī)

Generator 是實(shí)現(xiàn)狀態(tài)機(jī)的最佳結(jié)構(gòu)。比如,下面的clock函數(shù)就是一個狀態(tài)機(jī)。

var ticking = true;
var clock = function() {
  if (ticking)
    console.log('Tick!');
  else
    console.log('Tock!');
  ticking = !ticking;
}

上面代碼的 clock 函數(shù)一共有兩種狀態(tài)( Tick 和 Tock ),每運(yùn)行一次,就改變一次狀態(tài)。這個函數(shù)如果用 Generator 實(shí)現(xiàn),就是下面這樣。

var clock = function* () {
  while (true) {
    console.log('Tick!');
    yield;
    console.log('Tock!');
    yield;
  }
};

上面的 Generator 實(shí)現(xiàn)與 ES5 實(shí)現(xiàn)對比,可以看到少了用來保存狀態(tài)的外部變量 ticking ,這樣就更簡潔,更安全(狀態(tài)不會被非法篡改)、更符合函數(shù)式編程的思想,在寫法上也更優(yōu)雅。Generator 之所以可以不用外部變量保存狀態(tài),是因?yàn)樗旧砭桶艘粋€狀態(tài)信息,即目前是否處于暫停態(tài)。

Generator 與協(xié)程

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

(1)協(xié)程與子例程的差異

傳統(tǒng)的“子例程”(subroutine)采用堆棧式“后進(jìn)先出”的執(zhí)行方式,只有當(dāng)調(diào)用的子函數(shù)完全執(zhí)行完畢,才會結(jié)束執(zhí)行父函數(shù)。協(xié)程與其不同,多個線程(單線程情況下,即多個函數(shù))可以并行執(zhí)行,但是只有一個線程(或函數(shù))處于正在運(yùn)行的狀態(tài),其他線程(或函數(shù))都處于暫停態(tài)(suspended),線程(或函數(shù))之間可以交換執(zhí)行權(quán)。也就是說,一個線程(或函數(shù))執(zhí)行到一半,可以暫停執(zhí)行,將執(zhí)行權(quán)交給另一個線程(或函數(shù)),等到稍后收回執(zhí)行權(quán)的時候,再恢復(fù)執(zhí)行。這種可以并行執(zhí)行、交換執(zhí)行權(quán)的線程(或函數(shù)),就稱為協(xié)程。

從實(shí)現(xiàn)上看,在內(nèi)存中,子例程只使用一個棧(stack),而協(xié)程是同時存在多個棧,但只有一個棧是在運(yùn)行狀態(tài),也就是說,協(xié)程是以多占用內(nèi)存為代價,實(shí)現(xiàn)多任務(wù)的并行。

(2)協(xié)程與普通線程的差異

不難看出,協(xié)程適合用于多任務(wù)運(yùn)行的環(huán)境。在這個意義上,它與普通的線程很相似,都有自己的執(zhí)行上下文、可以分享全局變量。它們的不同之處在于,同一時間可以有多個線程處于運(yùn)行狀態(tài),但是運(yùn)行的協(xié)程只能有一個,其他協(xié)程都處于暫停狀態(tài)。此外,普通的線程是搶先式的,到底哪個線程優(yōu)先得到資源,必須由運(yùn)行環(huán)境決定,但是協(xié)程是合作式的,執(zhí)行權(quán)由協(xié)程自己分配。

由于 JavaScript 是單線程語言,只能保持一個調(diào)用棧。引入?yún)f(xié)程以后,每個任務(wù)可以保持自己的調(diào)用棧。這樣做的最大好處,就是拋出錯誤的時候,可以找到原始的調(diào)用棧。不至于像異步操作的回調(diào)函數(shù)那樣,一旦出錯,原始的調(diào)用棧早就結(jié)束。

Generator 函數(shù)是 ES6 對協(xié)程的實(shí)現(xiàn),但屬于不完全實(shí)現(xiàn)。Generator 函數(shù)被稱為“半?yún)f(xié)程”(semi-coroutine),意思是只有 Generator 函數(shù)的調(diào)用者,才能將程序的執(zhí)行權(quán)還給 Generator 函數(shù)。如果是完全執(zhí)行的協(xié)程,任何函數(shù)都可以讓暫停的協(xié)程繼續(xù)執(zhí)行。

如果將 Generator 函數(shù)當(dāng)作協(xié)程,完全可以將多個需要互相協(xié)作的任務(wù)寫成 Generator 函數(shù),它們之間使用 yield 表達(dá)式交換控制權(quán)。

Generator 與上下文

JavaScript 代碼運(yùn)行時,會產(chǎn)生一個全局的上下文環(huán)境(context,又稱運(yùn)行環(huán)境),包含了當(dāng)前所有的變量和對象。然后,執(zhí)行函數(shù)(或塊級代碼)的時候,又會在當(dāng)前上下文環(huán)境的上層,產(chǎn)生一個函數(shù)運(yùn)行的上下文,變成當(dāng)前(active)的上下文,由此形成一個上下文環(huán)境的堆棧(context stack)。

這個堆棧是“后進(jìn)先出”的數(shù)據(jù)結(jié)構(gòu),最后產(chǎn)生的上下文環(huán)境首先執(zhí)行完成,退出堆棧,然后再執(zhí)行完成它下層的上下文,直至所有代碼執(zhí)行完成,堆棧清空。

Generator 函數(shù)不是這樣,它執(zhí)行產(chǎn)生的上下文環(huán)境,一旦遇到 yield 命令,就會暫時退出堆棧,但是并不消失,里面的所有變量和對象會凍結(jié)在當(dāng)前狀態(tài)。等到對它執(zhí)行 next 命令時,這個上下文環(huán)境又會重新加入調(diào)用棧,凍結(jié)的變量和對象恢復(fù)執(zhí)行。

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


let g = gen();


console.log(
  g.next().value,
  g.next().value,
);

上面代碼中,第一次執(zhí)行 g.next() 時,Generator 函數(shù) gen 的上下文會加入堆棧,即開始運(yùn)行 gen 內(nèi)部的代碼。等遇到 yield 1 時, gen 上下文退出堆棧,內(nèi)部狀態(tài)凍結(jié)。第二次執(zhí)行 g.next() 時, gen 上下文重新加入堆棧,變成當(dāng)前的上下文,重新恢復(fù)執(zhí)行。

11. 應(yīng)用

Generator可以暫停函數(shù)執(zhí)行,返回任意表達(dá)式的值。這種特點(diǎn)使得 Generator 有多種應(yīng)用場景。

(1)異步操作的同步化表達(dá)

Generator 函數(shù)的暫停執(zhí)行的效果,意味著可以把異步操作寫在yield表達(dá)式里面,等到調(diào)用 next 方法時再往后執(zhí)行。這實(shí)際上等同于不需要寫回調(diào)函數(shù)了,因?yàn)楫惒讲僮鞯暮罄m(xù)操作可以放在 yield 表達(dá)式下面,反正要等到調(diào)用 next 方法時再執(zhí)行。所以,Generator 函數(shù)的一個重要實(shí)際意義就是用來處理異步操作,改寫回調(diào)函數(shù)。

function* loadUI() {
  showLoadingScreen();
  yield loadUIDataAsynchronously();
  hideLoadingScreen();
}
var loader = loadUI();
// 加載UI
loader.next()


// 卸載UI
loader.next()

上面代碼中,第一次調(diào)用 loadUI 函數(shù)時,該函數(shù)不會執(zhí)行,僅返回一個遍歷器。下一次對該遍歷器調(diào)用 next 方法,則會顯示 Loading 界面( showLoadingScreen ),并且異步加載數(shù)據(jù)( loadUIDataAsynchronously )。等到數(shù)據(jù)加載完成,再一次使用 next 方法,則會隱藏 Loading 界面。可以看到,這種寫法的好處是所有 Loading 界面的邏輯,都被封裝在一個函數(shù),按部就班非常清晰。

Ajax 是典型的異步操作,通過 Generator 函數(shù)部署 Ajax 操作,可以用同步的方式表達(dá)。

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


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

上面代碼的 main 函數(shù),就是通過 Ajax 操作獲取數(shù)據(jù)。可以看到,除了多了一個 yield ,它幾乎與同步操作的寫法完全一樣。注意, makeAjaxCall 函數(shù)中的 next 方法,必須加上 response 參數(shù),因?yàn)?yield 表達(dá)式,本身是沒有值的,總是等于 undefined 。

下面是另一個例子,通過 Generator 函數(shù)逐行讀取文本文件。

function* numbers() {
  let file = new FileReader("numbers.txt");
  try {
    while(!file.eof) {
      yield parseInt(file.readLine(), 10);
    }
  } finally {
    file.close();
  }
}

上面代碼打開文本文件,使用 yield 表達(dá)式可以手動逐行讀取文件。

(2)控制流管理

如果有一個多步操作非常耗時,采用回調(diào)函數(shù),可能會寫成下面這樣。

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

上面代碼已經(jīng)把回調(diào)函數(shù),改成了直線執(zhí)行的形式,但是加入了大量 Promise 的語法。Generator 函數(shù)可以進(jìn)一步改善代碼運(yùn)行流程。

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
  }
}

然后,使用一個函數(shù),按次序自動執(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);
  }
}

注意,上面這種做法,只適合同步操作,即所有的 task 都必須是同步的,不能有異步操作。因?yàn)檫@里的代碼一得到返回值,就繼續(xù)往下執(zhí)行,沒有判斷異步操作何時完成。如果要控制異步的操作流程,詳見后面的《異步操作》一章。

下面,利用 for...of 循環(huán)會自動依次執(zhí)行 yield 命令的特性,提供一種更一般的控制流管理的方法。

let steps = [step1Func, step2Func, step3Func];


function* iterateSteps(steps){
  for (var i=0; i< steps.length; i++){
    var step = steps[i];
    yield step();
  }
}

上面代碼中,數(shù)組 steps 封裝了一個任務(wù)的多個步驟,Generator 函數(shù) iterateSteps 則是依次為這些步驟加上 yield 命令。

將任務(wù)分解成步驟之后,還可以將項(xiàng)目分解成多個依次執(zhí)行的任務(wù)。

let jobs = [job1, job2, job3];


function* iterateJobs(jobs){
  for (var i=0; i< jobs.length; i++){
    var job = jobs[i];
    yield* iterateSteps(job.steps);
  }
}

上面代碼中,數(shù)組 jobs 封裝了一個項(xiàng)目的多個任務(wù),Generator 函數(shù) iterateJobs 則是依次為這些任務(wù)加上 yield* 命令。

最后,就可以用 for...of 循環(huán)一次性依次執(zhí)行所有任務(wù)的所有步驟。

for (var step of iterateJobs(jobs)){
  console.log(step.id);
}

再次提醒,上面的做法只能用于所有步驟都是同步操作的情況,不能有異步操作的步驟。如果想要依次執(zhí)行異步的步驟,必須使用后面的《異步操作》一章介紹的方法。

for...of 的本質(zhì)是一個 while 循環(huán),所以上面的代碼實(shí)質(zhì)上執(zhí)行的是下面的邏輯。

var it = iterateJobs(jobs);
var res = it.next();


while (!res.done){
  var result = res.value;
  // ...
  res = it.next();
}

(3)部署 Iterator 接口

利用 Generator函數(shù),可以在任意對象上部署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

上述代碼中, myObj 是一個普通對象,通過 iterEntries 函數(shù),就有了 Iterator 接口。也就是說,可以在任意對象上部署 next 方法。

下面是一個對數(shù)組部署 Iterator 接口的例子,盡管數(shù)組原生具有這個接口。

function* makeSimpleGenerator(array){
  var nextIndex = 0;


  while(nextIndex < array.length){
    yield array[nextIndex++];
  }
}


var gen = makeSimpleGenerator(['yo', 'ya']);


gen.next().value // 'yo'
gen.next().value // 'ya'
gen.next().done  // true

(4)作為數(shù)據(jù)結(jié)構(gòu)

Generator 可以看作是數(shù)據(jù)結(jié)構(gòu),更確切地說,可以看作是一個數(shù)組結(jié)構(gòu),因?yàn)?Generator 函數(shù)可以返回一系列的值,這意味著它可以對任意表達(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');
}

上面代碼就是依次返回三個函數(shù),但是由于使用了 Generator 函數(shù),導(dǎo)致可以像處理數(shù)組那樣,處理這三個返回的函數(shù)。

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

實(shí)際上,如果用 ES5 表達(dá),完全可以用數(shù)組模擬 Generator 的這種用法。

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

上面的函數(shù),可以用一模一樣的 for...of 循環(huán)處理!兩相一比較,就不難看出 Generator 使得數(shù)據(jù)或者操作,具備了類似數(shù)組的接口。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號