1. 簡介html
Generator函數時ES6提供的一種異步編程解決方案。Generator語法行爲和普通函數徹底不一樣,咱們能夠把Generator理解爲一個包含了多個內部狀態的狀態機。node
執行Generator函數回返回一個遍歷器對象,也就是說Generator函數除了提供狀態機,還能夠生成遍歷器對象。Generator能夠此返回多個遍歷器對象,經過這個對象能夠訪問到Generator函數內部的多個狀態。git
形式上Generator函數和普通的函數有兩點不一樣,一是function關鍵字後面,函數名前面有一個星花符號「*」,二是,函數體內部使用yield定義(生產)不一樣的內部狀態。程序員
執行Generator函數返回的是一個遍歷器對象,這個對象上有一個next方法,執行next方法會返回一個對象,這個對象上有兩個屬性,一個是value,是yield關鍵字後面的表達式的值,一個是done,布爾類型,true表示沒有遇到return語句,能夠繼續往下執行,false表示遇到return語句。來看下面的語句:github
function* helloWorldGenerator () { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator(); console.log(hw.next()); //第一次調用,Generator函數開始執行,直到遇到yield表達式爲止。next方法返回一個對象,它的value屬性就是當前yield語句後面表達式的值hello,done屬性爲false,表示遍歷尚未結束 console.log(hw.next()); //第二次調用,Generator函數從上次yield表達式停下的地方,一直執行到下一個yield表達式。next方法返回的對象的value屬性就是當前yield語句後面表達式的值world,done屬性值爲false,表示遍歷尚未結束。 console.log(hw.next()); //第三次調用,Generator函數從上次yield表達式停下的地方,一直執行到return語句(若是沒有return語句,則value屬性爲undefined),done屬性爲true,表示遍歷已經執行結束。 console.log(hw.next()); //第四次調用,此時Generator函數已經執行完畢,next方法返回對戲那個的value屬性爲undefined,done屬性爲true,表示遍歷結束。 console.log(hw.next()); //第五次執行和第四次執行的結果是同樣的。
執行結果以下圖:算法
1. 定義Generator函數helloWorldGenerator函數
2. 函數內部有2個yield表達式和一個return語句,return語句結束執行
3. Generator函數的調用方法和普通函數同樣,也是在函數名後面加上一對圓括號。不一樣的是調用以後,函數不是當即執行,返回的也不是return語句的結果undefined,而是一個指向內部狀態的指針對象,也就是上面說的遍歷器對象(Iterator Object)
4. 調用遍歷器對象的next方法,狀態指針移動到下一個狀態,返回{value: "hello", done: false}
5. 調用遍歷器對象的next方法,狀態指針移動到下一個狀態,返回{value: "world", done: false}
6. 調用遍歷器對象的next方法,狀態指針移動到下一個狀態,返回{value: "ending", done: true},done爲true,說明已經遇到了return語句,後面已經沒有狀態能夠返回了
7. 調用遍歷器對象的next方法,指針再也不移動,返回{value: undefined, done: true}
8. 調用遍歷器對象的next方法,指針再也不移動,返回{value: undefined, done: true}shell
注意yield表達式後面的表達式,只有當調用next方法,內部指針指向該語句時纔會執行,至關於JavaScript提供了手動的「惰性求值」語法功能。npm
function* gen() { yield 123 + 456; }
上面代碼中,yield後面表達式123 + 456,不會當即求值,只會在next方法將指針移動到這一句時,纔會求值。編程
yield表達式語句和return語句有類似之處,也有卻別。類似的地方是都能返回緊跟在語句後面的那個表達式的值。卻別在於每次遇到yield,函數暫停執行,下一次再從該位置繼續向後執行,return語句沒有位置記憶功能。一個函數裏面,只能執行一次return語句,可是能夠屢次執行yield表達式。也就是說Generator能夠逐次生成多個返回值,這也是它的名字的來歷。 json
Generator函數中能夠不用yield表達式,這時就變成了一個單純的暫緩執行函數。看下面代碼:
function* f () { console.log('執行了!') } var generator = f(); setTimeout(function () { console.log(generator.next()); // 執行Generator函數,只到遇到yield表達式,這裏沒有就直接輸出:"執行了!",函數返回{"done":true}沒有value }, 2000);
輸出結果以下:
Generator函數f()中沒有yield表達式,可是仍然仍是一個Generator函數。若是函數f是一個普通函數,在執行var generator = f();的時候就會輸出「執行了!」。可是f()是一個Generator函數,就變成了只有調用next方法的時候,函數f纔會執行。
另外須要注意,yield表達式只能用在Generator函數裏面,用在其餘地方都會報錯。看下面的代碼:
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 (let f of flat) { console.log(f); }
上面代碼會報錯,由於forEach方法的參數是一個普通函數,可是在裏面使用了yield表達式。能夠把forEach改爲for循環
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); }
輸出結果以下:
另外,若是yield表達式用在另一個表達式之中,必須放在圓括號內部。以下:
function *demo() { console.log('hello ' + (yield)); console.log('world ' + (yield 123)); } var gen = demo(); console.log(gen.next()); console.log(gen.next()); console.log(gen.next());
輸出結果以下:
1. 定義Generator函數demo
2. 函數內部有輸出"hello"+(yield)和「world」+(yield 123)
3. 調用demo方法獲得遍歷器對象gen
4. 調用遍歷器對象的next方法並輸出,注意先執行表達式語句「hello」 + (yield),獲得{value: undefined, done: false},再輸出:「hello undefined」。注意直接輸出yield表達式獲得的結果是undefined,必須使用遍歷器對象的next方法才能獲取yield表達式後面的值
5. 調用遍歷器對象的next方法並輸出,注意先執行表達式語句「worold」 + (yield),獲得{value: 123, done: false},再輸出:「world undefined」。注意直接輸出yield表達式獲得的結果是undefined,必須使用遍歷器對象的next方法才能獲取yield表達式後面的值
6. 調用遍歷器對象的next方法,由於後面已經沒有yield表達式,雖然沒有return語句,判斷依據是否有更多的yield語句爲標準,仍是輸出{value: undefined, done: true}。done的值是true。後面不管調用next方法多少次,都是這個結果。
yield表達式用做函數或者放在賦值表達式的右邊,能夠不加括號。以下:
function* demo() { foo(yield 'a', yield 'b'); // OK let input = yield; // OK }
上面說到yield表達式自己輸出的是undefined,也就是說yield表達式自己沒有返回值,或者說老是返回undefined。next方法能夠帶一個參數,該參數會被當作上一個yield表達式的返回值。
function *f() { for(var i=0; true; i++){ var reset = yield i; if(reset) { i = -1 } } } var g = f(); console.log(g.next()); console.log(g.next()); console.log(g.next(true));
返回結果以下:
上面代碼返回一個能夠無限運行的Generator函數f,若是next方法沒有參數,每次運行到yield表達式,變量reset的值老是yield表達式的值undefined。當next方法帶一個參數true時,變量reset就被重置爲這個參數的值,即true,所以i的值會等於-1,下一輪循環就會從-1開始遞增。這個功能有很重要的語法意義。Generator函數從暫停狀態到恢復運行,它的上下文狀態(context)是不變的。經過next方法的參數,就有辦法在Generator函數開始運行以後,繼續向函數體內部注入值。也就是說,在Generator函數運行的不一樣階段,從外部向內部注入不一樣的值,能夠調整函數行爲。
看下面的例子:
function* foo (x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z); } var a = foo(5); console.log(a.next()); console.log(a.next()); console.log(a.next()); var b = foo(5); console.log(b.next()); console.log(b.next(12)); console.log(b.next(13));
運行結果以下圖:
1. 申明一個Generator函數foo
2. 調用函數foo,傳入參數5,獲得遍歷器對象a
3. 調用遍歷器對象a的next方法,返回yield關鍵字後面表達式(x + 1)的值,獲得6,返回結果{ value: 6, done: false }。
4. 調用遍歷器對象a的next方法,往下執行,由於執行next的時候沒帶參數,上一次yield表達式的值從6變成undefined,而不是6,y的值是2 * undefined,即爲NaN。本次yield表達式的值是undefined / 3 爲NaN。最後返回結果{ value: undefined, done: false }
5. 調用遍歷器對象a的next方法,往下執行,由於執行next的時候沒有帶參數,上一次yield表達式的值爲從NaN變成undefined,所以z的值是undefined,返回的值爲5 + NaN + undefined,即爲NaN
6. 調用函數foo,傳入參數5,獲得遍歷器對象b
7. 調用遍歷器對象的next方法,返回yield關鍵字後面表達式(x + 1)的值,獲得6,返回結果{ value: 6, done: false }
8. 調用遍歷器對象的next方法,傳參12,所以上一次yield關鍵字後面的表達式的(x + 1)的值從6變爲12,y的值是2 * 12,即爲24。yield關鍵字後面表達式的值爲 (24 / 3),即爲8。最後返回結果{ value: 8, done: false }
9. 調用遍歷器對象的next方法,傳入參數13,所以上一次yield關鍵字後面的表達式(y / 3)的值從8變成13,z的值是13。表達式(x + y + z)的值是(5 + 24 + 13),即42。最後返回結果{ value: 24, done: true }
注意,因爲next方法的參數表示上一個yield表達式的返回值,因此在第一調用next方法時,傳遞參數是無效的。JavaScript引擎直接忽略第一次使用next方法時的參數,只有從第二次使用next方法開始,參數纔是有效的。從語義上說,第一個next方法用來啓動遍歷器對象,因此不用帶參數。
再看一個例子:
function * dataConsumer () { console.log('started') console.log(`1.${yield }`) console.log(`2.${yield }`) return 'result' } let genObj = dataConsumer() genObj.next() genObj.next('a') genObj.next('b')
輸出結果:
1. 定義Generator函數dataConsumer
2. 調用dataConsumer函數,獲得遍歷器對象genObj
3. 調用遍歷器對象genObj的next方法,執行執行dataConsumer函數,只到遇到yield表達式爲止。注意第一句輸出「started」,第二句裏就有yield表達式,所以在這裏中止。最終結果是「started」
4. 調用遍歷器對象genObj的next方法,傳入參數‘a’,繼續往下執行,上一次yield表達式的值從undefined變成‘a’,最後輸出1.a
5. 調用遍歷器對象genObj的next方法,傳入參數‘b’,繼續往下執行,上一次yield表達式的值從undefined變成‘b’,最後輸出2.b
上面代碼是一個很直觀的例子,每次經過next方法向Generator函數注入值,而後打印出來。
若是想要第一次調用next方法時就可以輸入值,能夠在Generator函數外面再包一層。
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')
輸出結果:
上面代碼中,Generator函數若是不用wrapper先包一層,是沒法在第一次調用next方法的時候就輸入參數的。 這個其實在包裝函數wrapper裏面已經先執行了一次next方法了。
2. Generator和Iterator接口的關係
任意一對象的Symbol.iterator方法,等於該對象的遍歷器生成函數,調用該函數會返回該對象的一個遍歷器對象。
因爲Generator函數就是遍歷器生成函數,所以能夠把Generator賦值給對象的Symbol.iterator屬性,從而使這個對象具備Iterator接口。
var myIterable = {}; myIterable[Symbol.iterator] = function *() { yield 1; yield 2; yield 3; }; console.log([...myIterable]);
輸出結果以下:
上面代碼中Generator函數賦值給Symbol.iterator屬性,從而使myIterator對象具備了iterator接口,這樣就能夠被...運算符遍歷了。
Generator函數執行後,返回一個遍歷器對象。該對象自己也具備Symbol.iterator屬性,執行後返回自身。
function *gen() { } var g = gen(); console.log(g[Symbol.iterator]() === g); // 輸出true
上面代碼中,gen是一個Generator函數,調用它會生成一個遍歷器對象g,它的Symbol.iterator屬性也是一個遍歷器對象生成函數,執行後返回它本身。
for...of循環能夠自動遍歷Generator函數生成的Generator對象,而且不須要調用next方法,看下面的代碼:
function* foo () { yield 1; yield 2; yield 3; yield 4; yield 5; return 6; } for (let v of foo()) { console.log(v); }
輸出結果以下:
上面代碼中使用for...of循環,以此顯示5個yield表達式的值。這裏須要注意,一旦next方法的返回對象的done屬性爲true,for...of循環就會終止,且不包含該返回對象,因此上面代碼中return語句返回值6,不包括在for...of循環中。
下面是一個利用Generator函數和for...of循環,實現斐波那契數列的例子
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語句時,再也不須要使用next方法。注意這裏用for...of循環代替了next方法,照樣能夠執行Generator函數。
利用for...of循環,能夠寫出遍歷任意對象(Object)的方法。原生的JavaScript兌現更沒有遍歷接口,沒法使用for...of循環,經過Generator函數爲它加上這個接口,就可使用了。
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}`); }
輸出結果以下:
上面代碼中,原生對象jane不具有Iterator接口,沒法用for...of遍歷。這是咱們經過Generator函數objectEntries爲它加上遍歷器接口,就能夠用for...of遍歷了。加上遍歷器接口的另外一種寫法是,將Generator函數加到對象的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}`); }
輸出結果以下:
除了for...of循環,擴展運算符(...),解構賦值和Array.from方法內部調用的都是遍歷器接口,這就是說,它們均可以將Generator函數返回的Iterator對象做爲參數。看下面的代碼:
function* numbers () { yield 1 yield 2 return 3 yield 4 } // 擴展運算符 console.log(...numbers()) // Array.from方法 console.log(Array.from(numbers())) // 解構賦值 let [x, y] = numbers() console.log(x, y) // for ... of循環 for (let n of numbers()) { console.log(n) }
輸出結果以下:
3. Generator.property上的方法
3.1. Generator.property.throw()
Generator原型對象上有一個throw方法,能夠在函數體外拋出錯誤,而後在Generator函數體內捕獲。
var g = function* () { try { yield; } catch (e) { console.log('內部捕獲', e) } } var i = g(); i.next(); try { i.throw('a'); i.throw('b'); } catch (e) { console.log('外部捕獲', e); }
輸出結果以下:
上面代碼中,遍歷器對象i連續拋出兩個錯誤。第一個錯誤被Generator函數體內部的catch語句捕獲。i第二次拋出錯誤,因爲Generator函數內部的catch語句已經執行過了,不會再捕捉到這個錯誤,因此這個錯誤就被拋出了Generator函數體,被函數體外的catch語句捕獲。
throw方法能夠接受一個參數,參數會被catch語句接收,建議拋出Error對象實例。
var g = function* () { try { yield; } catch (e) { console.log(e) } } var i = g(); i.next(); i.throw(new Error('出錯了!'))
輸出結果以下:
不要混淆遍歷器對象的throw方法和全局的throw命令。上面代碼的錯誤,是用遍歷器對象的throw方法拋出的,而不是用throw命令拋出的。後者只能被函數體外的catch語句捕獲。
var g = function* () { while (true) { try { yield; } catch (e) { if (e !== 'a') { throw e; } console.log('內部捕獲', e) } } } var i = g(); i.next(); try { throw new Error('a'); throw new Error('b'); } catch (e) { console.log('外部捕獲', e); }
輸出結果以下:
上面代碼只捕獲了a,是由於函數體外的catch語句塊,捕獲了拋出的a錯誤之後,就不會再繼續try代碼塊裏剩餘的語句了。由於沒有執行i.catch()語句,內部的異常不會被捕獲。
若是Generator函數內部沒有try...catch代碼塊,那麼throw方法拋出的錯誤將被外部try...catch代碼塊捕獲。
var gen = function* gen () { yield console.log('hello'); yield console.log('world'); } var g = gen(); g.next(); g.throw();
輸出以下:
上面代碼中給,g.throw拋出錯誤後,沒有任何try...catch代碼能夠捕獲這個錯誤,致使程序報錯,終端執行。
throw方法拋出的錯誤要被內部捕獲,前提是必須至少執行一次next方法。
function * gen () { try { yield 1 } catch (e) { console.log('內部捕獲') } } var g = gen() t.throw(1)
輸出結果以下:
上面代碼中,g.throw(1)執行時,next方法一次都沒有執行過。這時,拋出的錯誤不會被內部捕獲,而是直接在外部拋出,致使程序出錯。這種行爲其實很好理解,由於第一次執行next方法,等同於啓動執行Generator函數的內部代碼,不然Generator函數尚未開始執行,這時throw方法拋出錯誤只能拋出在函數外部。
throw方法被捕獲之後,會附帶執行下一條yield表達式。也就是說,會附帶執行一次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(); g.throw(); g.next();
輸出結果以下:
上面代碼中,g.throw方法被捕獲之後,自動執行了一次next方法。因此會打印b。另外,也能夠看到,只要Generator函數內部部署了try...catch代碼塊,那麼遍歷器的throw方法拋出的錯誤,不影響下一次遍歷。
另外,throw命令和g.throw方法是無關的,二者互不影響。
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(); }
輸出結果以下:
上面代碼中,throw命令拋出的錯誤不會影響到遍歷器的狀態,因此兩次執行next方法,都進行了正確的操做。
這種函數體內捕獲錯誤的機制,方便了對錯誤的處理。多個yield表達式能夠只用一個try...catch代碼塊來捕獲錯誤。若是使用回調函數的寫法,想要捕獲多個錯誤,就不得不爲每一個函數內部寫一個錯誤處理語句,如今只在Generator函數內部洗寫一次catch語句就能夠了。
Generator函數體外拋出的錯誤,能夠在函數體內捕獲;反過來,Generator函數體內拋出的錯誤,也可被函數體外的catch捕獲。
function* foo () { var x = yield 3; var y = x.toUpperCase(); yield y; } var it = foo(); it.next(); try { it.next(42); } catch (err) { console.log(err); }
上面代碼中,第二個next方法向函數體內傳入一個參數42,,數值是沒有toUpperCase方法的,因此會拋出一個TypeError錯誤,被函數體外的catch捕獲。
一旦Generator執行過程當中拋出錯誤,且沒有被內部捕獲,就不會再往下執行下去了。若是還調用next方法,將返回一個value屬性爲undefined,done屬性爲tru的對象,即JavaScript引擎認爲這個Generator已經運行結束了。
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('第一次運行next方法', v); } catch(err) { console.log('捕捉錯誤', v); } try{ v = generator.next(); console.log('第二次運行next方法',v); }catch(err){ console.log('捕捉錯誤', v); } try{ v = generator.next(); console.log('第三次運行next方法', v); } catch(err) { console.log('捕捉錯誤', v); } console.log('caller done'); } log(g());
執行結果以下:
上面代碼一共三次運行next方法,第二次運行的時候會拋出錯誤,而後第三次運行的時候,Generator函數就已經結束了,再也不執行下去。
3.2.Generator.property.return()
Generator函數返回的遍歷器對象,還有一個return方法,能夠返回給定值,而且終結遍歷Generator函數。
function *gen() { yield 1; yield 2; yield 3; } var g = gen(); console.log(g.next()); console.log(g.return('foo')); console.log(g.next());
執行結果以下:
上面代碼中,遍歷器對象g調用return方法以後,返回值的value屬性就是return方法的參數「foo」。而且,Generator函數的遍歷就終止了,返回值的done屬性爲true,之後再調用next方法,done屬性的返回值老是true。
若是return方法調用時,不提供參數,則返回值的value屬性爲undefined。
function *gen() { yield 1; yield 2; yield 3; } var g = gen(); console.log(g.next()); console.log(g.return());
執行結果以下:
若是Generator函數內部有try...finally代碼塊,那麼return方法會推遲到finally代碼塊執行完後再執行。
function * numbers() { yield 1; try{ yield 2; yield 3; } finally { yield 4; yield 5; } yield 6; } var g = numbers(); console.log(g.next()); console.log(g.next()); console.log(g.return(7)); console.log(g.next()); console.log(g.next());
執行結果以下:
上面代碼中,調用return方法後,就開始執行finally代碼塊,而後等到finally代碼塊執行完,再執行return方法。 遇到return語句對象指針就會跳轉到finally裏去執行,只到把finally裏的語句執行完再執行return語句。
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) console.log(gen.next()) console.log(gen.next(1))
輸出結果以下:
上面代碼中,第二個next(1)方法至關於將yield表達式x + y替換成一個值1。若是next方法沒有參數,就至關於替換成undefined。因此第二次調用next方法的時候若是不傳參數,返回的結果是{ value: undefined, done: false }。
throw是將yield表達式替換成一個throw語句。
const g = function* (x, y) { let result = yield x + y return result } const gen = g(1, 2) console.log(gen.next()) gen.throw(new Error('出錯了'))
輸出結果以下:
上面代碼至關於將let result = yield x + y替換成let result = throw(new Error('出錯了'))
return語句時將yield表達式替換成一個return語句
const g = function* (x, y) { let result = yield x + y return result } const gen = g(1, 2) console.log(gen.next()) console.log(gen.return(2))
輸出結果以下:
return語句至關於將let result = yield x + y替換成let result = return 2
4. yield*表達式
若是在Generator函數內部,調用跟另一個Generator函數,默認狀況下是沒有效果的。看下面代碼:
function* foo () { yield 'a'; yield 'b'; } function* bar () { yield 'x'; foo(); yield 'y'; } for (let v of bar()) { console.log(v); }
輸出結果以下
上面代碼中,foo和bar都是Generator函數,在bar函數中調用foo,是不會有任何效果的。可使用yield*表達式來調用另一個Generator函數。以下代碼:
function* foo () { yield 'a'; yield 'b'; } function* bar () { yield 'x'; yield *foo(); yield 'y'; } for (let v of bar()) { console.log(v); }
執行效果以下:
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); }
輸出結果是相同的,在一個Generator函數中使用yield*調用另一個Generator函數,至關於把另外一個Generator函數中的yield表達式放在這個函數中執行。
function* inner () { yield 'hello!'; } function* outter1 () { yield 'open'; yield inner(); yield 'close'; } var gen = outter1(); console.log(gen.next().value); console.log(gen.next().value); console.log(gen.next().value); function* outter2 () { yield 'open'; yield* inner(); yield 'close'; } var gen2 = outter2(); console.log(gen2.next().value); console.log(gen2.next().value); console.log(gen2.next().value);
輸出結果以下:
上面代碼中,outer2使用了yield*表達式,outer1沒有使用。結果就是,outer1返回一個遍歷器對象,outer2返回該遍歷器對象的內部值。從語法角度看,若是yield表達式後面跟的是一個遍歷器對象,須要在yield關鍵字後面加上星號,代表它返回的是一個遍歷器對象,這被稱爲yield*表達式。
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); }
執行結果以下:
上面代碼中,delegatingIterator是代理者,delegatedIterator是被代理者,因爲yield* delegatedIterator語句獲得的值,是一個遍歷器,因此要用星號表示。運行結果是用一個遍歷器遍歷了多個Generator,有遞歸的效果。
yield*後面的Generator函數沒有return語句的時候,等同於在Generator函數內部,部署了一個for...of循環。以下代碼:
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函數(沒有return語句時),不過是for...of的一種簡寫形式,徹底能夠用後者代替前者。反之,在有return語句的時候須要使用var value = yield* iterator的形式獲取return語句的值。
若是yield*後面跟着一個數組,因爲數組原生支持遍歷器,所以就會遍歷數組成員。以下代碼:
function* gen () { yield* ['a', 'b', 'c'] } let g = gen() console.log(g.next()) console.log(g.next()) console.log(g.next()) console.log(g.next())
執行結果以下:
上面代碼中,yield命令後面若是不加星號,返回的是整個數組,加了星號就表示返回的是數組的遍歷器對象。
實際上,任何數據結構,只要有Iterator接口,就能夠被yield*表達式遍歷。
let read = (function* () { yield 'hello'; yield* 'world'; })(); console.log(read.next().value); console.log(read.next().value);
返回結果以下:
上面代碼中,yield表達式返回的是整個字符串,可是yield*表達式返回的是單個字符。由於字符串有Iterator接口,因此被yield*表達式遍歷。
若是被代理的Generator函數有return語句,那麼就能夠向代理它的Generator函數返回數據。
function* foo () { yield 2; yield 3; return "foo"; yield 4; } function* bar () { yield 1; var v = yield* foo(); console.log("v: " + v); yield 5; } var it = bar(); 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()); // "v: foo" {value: 5, done: true} console.log(it.next()); // {value: undefined, done: true}
執行結果以下:
1. 定義Generator函數foo
2. 定義Generator函數bar,在函數內部使用yield*表達式調用函數foo
3. 調用bar方法,獲得遍歷器對象it
4. 調用遍歷器對象it的next方法,返回{ value: 1, done: false }
5. 調用遍歷器對象it的next方法,返回Generator函數foo的第一個yield表達式返回的對象{ value: 2, done: false }
6. 調用遍歷器對象it的next方法,返回Generator函數foo的第二個yield表達式返回的對象{ value: 3, done: false }
7. 調用遍歷器對象it的next方法,foo結束,foo方法裏面有return語句,返回值是「foo」,繼續往下執行只到遇到yield語句,輸出「v:foo」 並輸出{ value: 5, done: false }
8. 調用遍歷器對象it的next方法,Generator函數裏已經沒有yield語句,輸出{ value: undefiined, done: true }
再看下面的例子
function* genFuncWithReturn () { yield 'a'; yield 'b'; return 'The result'; } function* logReturned (genObj) { let result = yield* genObj; console.log(result); } console.log([...logReturned(genFuncWithReturn())])
輸出結果:
上面代碼中,存在兩次遍歷,第一是擴展運算符便利函數logReturned返回的遍歷器對象,第二次是yield*語句遍歷函數genFunWithReturn返回的遍歷器對象。這兩次遍歷效果是疊加的,最終表現爲擴展運算符遍歷函數getFuncWithReturn返回的遍歷器對象。這兩次遍歷的效果是疊加的,最終表現爲擴展運算符遍歷函數getFunWithReturn返回的遍歷器對象。因此,最後的數據表達式獲得的值是[ 'a', 'b' ]。可是函數getFuncWithReturn的return語句的返回值「The result」,會返回給函數logReturned內部的result變量,所以會有終端輸出。
yield*命令能夠很方便地取出多維嵌套數組的全部成員。看下面的代碼:
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); }
運行結果以下,效果至關於Array.property.flat()
下面的例子稍微複雜,使用yield*語句遍歷徹底二叉樹
function Tree (left, label, right) { this.left = left; this.label = label; this.right = right; } // 下面是中序(inorder)遍歷函數,因爲返回的是一個遍歷器,因此要用genrator函數,函數體內採用遞歸算法,因此左樹和右樹都要用yield*遍歷 function* inorder (t) { if (t) { yield* inorder(t.left); yield t.label; yield* inorder(t.right) } } // 下面生成二叉樹 function make (array) { // 判斷是否爲葉子節點 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); } console.log(result);
輸出結果以下
做爲對象屬性的Generator函數
若是一個對象的屬性是Generator函數,能夠簡寫成下面的形式
let obj = { * myGeneratorMethod () { } }
上面代碼中myGeneratorMethod屬性前面有一個星號,表示這個屬性是一個Generator函數。它的完整形式以下:
let obj = { myGeneratorMethod: function* () { } }
Generator函數的this
Generator函數老是返回一個遍歷器,ES6規定這個遍歷器是Generator函數的實例,也繼承了Generator函數的prototype對象上的方法。
function* g () { } g.prototype.hello = function () { return 'hi!'; }; let obj = g(); console.log(obj instanceof g); // true console.log(obj.hello()); // 'hi'
上面代碼中,Generator函數g返回的遍歷器obj,是g的實例,並且繼承了g.prototype。可是,若是把g當作普通的構造函數,並不會生效,由於g返回老是遍歷器對象,而不是this對象。
function* g () { this.a = 11; } let obj = g(); console.log(obj.a); // undefined
上面代碼中,Generator函數g在this對象上面添加了一個屬性a,可是obj對象拿不到這個屬性。
Generator函數也不能和new命令一塊兒使用,不然會報錯。
function* F () { yield this.x = 2; yield this.y = 3; } let obj = new F(); // Uncaught TypeError: F is not a constructor
上面代碼中,new命令和Generator函數F一塊兒使用,結果報錯,由於F不是一個構造函數 。
有沒有辦法讓Generator函數返回一個正常的對象實例,既能夠用next方法,又能夠得到正常的this呢?下面是一個變通方法,首先生成一個空對象,使用call方法綁定Generator函數內部的this。這樣,構造函數調用之後,這個空對象就是Generator函數實例的對象了。
function* F () { this.a = 1; yield this.b = 2; yield this.c = 3; } var obj = {}; var f = F.call(obj); console.log(f.next()); // object{value: 2, done: false} console.log(f.next()); // object{value: 3, done: false} console.log(f.next()); // object{value: undefined, done: true} console.log(obj.a); //1 console.log(obj.b); //2 console.log(obj.c); //3
執行結果以下:
上面代碼中,首先是F內部的this對象綁定obj對象,而後調用它,返回一個Iterator對象。這個對象執行三次next()方法,(由於F內部有兩個yield表達式),完成F內部全部代碼運行。這是全部內部屬性都綁定在obj對象上路,所以obj對象也就成了F對象的實例。
上面代碼中給,執行的是遍歷器對象f,可是生成的對象實例是obj,有沒有辦法將這兩個對象統一塊兒來呢?一個辦法就是將obj換成F.prototype。
function* F () { this.a = 1; yield this.b = 2; yield this.c = 3; } var f = F.call(F.prototype); console.log(f.next()); console.log(f.next()); console.log(f.next()); console.log(f.a); console.log(f.b); console.log(f.c);
執行結果以下:
再將F改形成一個構造函數,就能夠對它執行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(); console.log(f.next()); console.log(f.next()); console.log(f.next()); console.log(f.a); console.log(f.b); console.log(f.c);
5 含義
5.1 Generator和狀態機
Generator是實現狀態機的最佳結果。好比,下面代碼中clock函數就是一個狀態機。
var ticking = true; var clock = function () { if (ticking) { console.log('Tick!') } else { console.log('Tock!') } ticking = !ticking; } clock(); clock();
clock函數有兩種狀態(Tick和Tock) ,每運行一次,就改變一次狀態。這個函數若是用Generator函數實現,就是像下面這樣:
var clock = function* () { while (true) { console.log('Tick!'); yield; console.log('Tock!'); yield; } }; var c = clock(); c.next(); c.next();
和上面不用Generator函數的方法比較,少了用來保存狀態的外部變量ticking,這樣更加簡潔,安全(狀態不會被外面代碼篡改) ,更符合函數式編程的思想,在寫法上也更加優雅。Generator之因此能夠不用外部變量保存,由於它自己就包含了一個狀態信息,即目前是否處於暫停狀態。
5.2 Generator與協程
協程(coroutine)是一種程序運行的方式,能夠理解爲「協做的線程」或者「協做的函數」。協程能夠用單線程實現,也能夠用多線程實現。
(1)協程與子例程的差別
傳統的「子例程」(subroutine)採用堆棧式的「後進先出」的執行方式,只有當調用的子函數徹底執行完畢,纔會結束執行父函數。協程與其不一樣,多個線程(單線程狀況下,即多個函數)能夠並行執行,可是隻有一個線程(或函數)處於正在運行的狀態,其餘線程(或函數)都處於暫停狀態(suspended),線程(或函數)之間能夠交換執行權。也就是說,一個線程(或函數)執行到一半,能夠暫停執行,將執行權交給另外一個線程(或函數),等到收回執行權的時候,再恢復執行。這種能夠並行執行,交換執行權的線程(或函數),就稱爲協程。
從實現上來看,在內存中給,子例程只使用一個棧(stack),而協程是同時存在多個棧,但只有一個棧是在運行狀態,也就是說,協程是以多佔用內存爲代碼,實現多任務的並行。
(2)協程與普通線程的差別
協程適用於多任務運行的環境。在這個意義上,它與普通的線程很類似,都有本身的執行上下文,能夠分享全局變量。他們的不一樣之處在於,同一時間能夠有多個線程處於運行狀態,可是同一時間運行的協程只有一個,其餘協程都處於暫停狀態。此外,普通的線程是搶先式的,到底哪一個線程優先獲得資源,必須由運行環境決定,可是協程是合做式的,執行權由協程本身分配。
因爲JavaScript是單線程語言,只能保持一個調用棧。引入協程後,每一個任務能夠保持本身的調用棧。這樣作的最大好處是拋出錯誤的時候,能夠找到原始的調用棧。不至於像異步操做的回調函數那樣,一旦出錯,原始的調用棧早就結束。
Generator函數是ES6對協程的實現,可是屬於不徹底實現。Generator函數是「半協程」,意思是隻有Generator函數的調用者,才能將程序的執行權交給Generator函數。若是是徹底執行的協程,任何函數均可以讓暫停的協程繼續執行。
若是將Generator函數當作協程,徹底能夠將多個須要相互協做的任務寫成Generator函數,他們之間使用yield表達式交換控制權。
5.3 Generator與上下文
JavaScript代碼運行時,會產生一個全局的上下文環境(context,又稱運行環境),它包含了當前全部變量和對象。而後,執行函數(或者塊級代碼)的時候,又會在當前上下文環境的上層,產生一個函數運行的上下文,變成當前(active)的上下文,由此產生一個上下文環境的堆棧(context statck)。
這個堆棧式「先進後出」的數據結構,最後產生的上下文環境首先執行完成,退出堆棧,而後執行完成它下層的上下文,直至全部代碼執行完成,堆棧清空。
Generator函數不是這樣,它執行產生的上下文環境,一旦遇到yied命令,就會暫時退出堆棧,可是並不消失,裏面全部變量和對象會凍結在當前狀態。等到對它執行next命令時,這個上下文環境又會從新加入調用棧,凍結的變量和對象恢復執行。
function * gen () { yield 1; return 2; } let g = gen(); console.log(g.next().value, g.next().value);
輸出結果:
上面的代碼中,第一次執行g.next()時,Generator函數的gen的上下文會加入堆棧,機開始運行gen內部的代碼。等到遇到yield 1的時候,gen上下文退出堆棧,內部狀態凍結。第二次執行g.next()時,gen上下文又從新加入堆棧,變成當前的上下文,從新恢復執行。
5.4 應用
Generator能夠暫停函數執行,返回yield表達式的值。這種特色使得Generator函數有多種應用場景。
5.4.1 異步操做的同步化表達
Generator函數的暫停執行的效果,意味着能夠把異步操做寫在yield表達式裏面,等到調用next方法時再日後執行。這實際上等同於不須要寫回調函數了。由於異步操做的後續操做能夠放在yield表達式下面,反正要等到調用next方法時再執行,因此Generator函數的一個重要時機意義就是用來處理異步操做,改寫回調函數。看下面的代碼段:
function* loadUI () { showLoadingScreen(); yield loadUIDataAsynchronously(); hideLoadingScreen(); } var loader = loadUI(); // 加載UI loader.next(); // 卸載UI loader.next();
上面代碼中,第一次調用loadUI函數時,該函數不會執行,僅返回一個遍歷器。下一次對改遍歷器調用next方法,則會顯示Loaidng界面(showLoadingScreen),而且異步加載數據(loadingUIDataAsynchronously)。等到數據加載完成,再一次調用next方法,則會隱藏Loading界面。能夠看到,這種寫法的好處是全部Loading界面的邏輯,都會被封裝在一個函數裏,循序漸進很是清晰。
下面是一個例子,能夠手動逐行讀取一個文本文件。
function* numbers () { let file = new FileReader('numbers.txt'); try { while (!file.eof) { yield parseInt(file.readLine(), 10); } } finally { file.close(); } }
5.4.2 流程控制
若是有一個多步操做很是耗時,採用回調函數,可能寫成下面這樣:
setp1(function (value1) { setp2(value1, function (value2) { setp3(value2, function (value3) { // Dom something with value3 }) }) })
採用Promise改寫上面的代碼。代碼中把回調函數,改爲直線執行的形式,可是加入了大量的Promise語法。
Promise.resolve(setp1) .then(setp2) .then(setp3) .then(setp4) .then(function (value4) { // Do something with value4 }, function (error) { // Handle any error from stemp1 through step4 }).done()
採用Generator語法,能夠寫成下面這樣:
function* longRunningTask (value1) { try { var value2 = yield step1(value1); var value3 = yield setp2(value2); var value4 = yield setp3(value3); var value5 = yield setp4(value4); // Do something with value4 } catch (e) { // handle error } } // 而後使用一個函數,依次自動執行全部步驟 scheduler(longRunningTask(initValue)); function scheduler (task) { var taskObj = task.next(task.value); if (!taskObj.done) { task.value = taskObj.value; scheduler(task); } }
注意,上面這種作法,只適合同步操做,即全部的task都必須是同步的,不能有異步操做。由於這裏的代碼獲得返回值,當即繼續往下執行,沒有判斷異步操做什麼時候完成。
下面使用for...of循環會一次執行yield命令的特性,提供一種更通常的控制流程管理的方式。
let step = [step1Func, setp2Func, setp3Fund]; function* iterateSteps () { for (var i = 0; i < step.length; i++) { var step = step[i]; yield step(); } }
上面代碼中,數組steps封裝了一個任務的多個步驟,Generator函數iterateSteps則是一次爲這些步驟添加上yield命令。
將任務分解成步驟以後,還能夠將項目分解成多個執行的任務。
let jobs = [job1, job2, job3]; function* iterateJobs (jobs) { for (var i = 0; i < jobs.length; i++) { var job = jobs[i]; yield* iterateSteps(job.setps); // 在Generator函數內部調用另一個Generator函數 } }
上面代碼中,數組jobs封裝了一個項目的多個任務,Generator函數iterateJobs則以此爲這些任務加上yield*命令。
最後,就可使用for...of循環以此執行全部任務的全部步驟。
for (var setp of iterateJobs(jobs)) { console.log(step.id); }
再次提醒,上面的作法只能用於全部步驟都是同步操做的狀況,不能有異步操做的步驟。
for...of本質是wihie循環,因此上面的代碼實質上執行的是下面的邏輯。
var it = iterateJobs(jobs); var res = it.next(); while (!res.done) { var result = res.value; res = it.next(); }
5.4.3 部署Iterator接口
利用Generator函數,能夠在任意對象上部署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); } for (let [key, value] of Object.entries(myObj)) { console.log(key, value); } for (let key in myObj) { console.log(key); }
輸出結果以下:
上面代碼中,myObj是一個普通對象,經過iterEntries函數,就有了Iterator接口。就是說能夠在任意對象上部署next方法。此外還可使用Object.keys,Object.values,Object.entries,for...in來遍歷對象
上述代碼中,myObj是一個普通對象,經過iterEntries函數,就有了Iterator接口。也就是說,能夠在任意對象上部署next方法。
下面例子是對數組部署Iterator接口的例子,儘管數組原生具備這個接口。
function * makeSimpleGenerator (array) { var nextIndex = 0 while (nextIndex < array.length) { yield array[nextIndex++] } } var gen = makeSimpleGenerator(['yo', 'ya']) console.log(gen.next()) console.log(gen.next()) console.log(gen.next()) console.log(gen.next())
執行結果以下:
5.4.4 做爲數據結構
Generator能夠看作是一個數據結果,更確切的說,能夠看作一個數組結構,由於Generator函數能夠返回一系列的值,這意味着它能夠對任意表達式,提供相似數組的接口。
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是一個函數,能夠像回調函數那樣使用它。 }
上面代碼就是一次返回三個函數,可是因爲使用了Generator函數,致使能夠像處理數組那樣,處理這三個返回的函數。
實際上,若是用ES5表達,徹底可使用數組模擬Generator的這種用法。
function doStuff () { return [ fs.readFile.bind(null, 'hello.txt'), fs.readFile.bind(null, 'world.txt'), fs.readFile.bind(null, 'and-such.txt') ] }
上面的函數,能夠用如出一轍的for...of循環處理。比較一下能夠看出Generator是的數組或者操做,具有了相似數組的接口。
6. 異步編程
6.1 概念
異步
異步編程在JavaScript語言中很重要。JavaScript語言的執行環境是「單線程的」的,若是沒有異步編程,不可思議。
ES6以前,異步編程的方法,大概是4種:回調函數,事件監聽,訂閱/發佈,Promise對象。Generator函數將JavaScript異步編程帶入一個全新的階段。
所謂「異步」,簡單的說就是一個任務不是連續完成的,能夠理解爲任務被認爲地分紅兩段,先執行第一段,而後轉而執行其餘的任務,等作好準備,再回頭執行第二段。
好比,有一個任務是讀取文件進行處理,任務的第一段向是操做系統發出請求,要求讀取文件。而後執行其餘任務,等到操做系統返回文件,再接着執行第二段(處理文件)。這種不連續的執行,就叫異步執行。
相應的,連續的執行就叫同步。因爲是連續執行,不能插入其餘任務,因此操做系統從硬盤讀取文件的這段時間,程序只能乾等着。
回調函數
JavaScript語言對異步編程的實現,就是回調函數。所謂回調函數,就是把任務的第二段單獨寫在一個函數裏,等到從新執行這個任務的時候,就直接調用這個函數。回調函數的英文名字是callback,是「從新調用」的意思。
讀取文件進行處理,是這樣的:
s.readFile('/etc/passwd', 'utf-8', function (err, data) { if (err) throw err; console.log(data); });
上面代碼中,readFile函數的第三個參數,就是回調函數,也就是任務的第二階段。等到操做系統返回了/etc/passwd這個文件以後,回調函數纔會執行。
一個有趣的問題是,爲何Node約定,回調函數的參數裏,必需要有一個錯誤對象err(若是沒有錯誤,這個參數是null)?
緣由是執行分紅兩段,第一段執行完成後,任務所在的上下文環境就已經結束了。在這之後拋出的錯誤,原來的上下文環境已經沒法捕捉,只能當作參數,傳入第二段。
Promise
回調函數自己並無問題,它的問題出在多個回調函數嵌套。假設讀取A文件以後,再讀取B文件,代碼以下:
fs.readFile(fileA, 'utf-8', function (err, data) { fs.readFile(fileB, 'utf-8', function (err, data) { // ... }); });
若是依次讀取兩個以上的文件,就會出現多重嵌套。代碼不是縱向發展,而是橫向發展,很快就會擠成一團,沒法閱讀。由於多個異步操做構成了強耦合,只要有一個操做須要修改,它的上層回調函數和下層回調函數,可能就要跟着修改,這種狀況被稱爲「回調函數地獄」。
Promise對象就是爲了解決這個問題而被提出的。它不是新的語法功能,而是一種新的寫法,容許將回調函數的嵌套,改爲鏈式調用。採用Promise,連續讀取多個文件的寫法以下:
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); });
上面代碼中,使用了fs-readfile-promise模塊,它的做用就是返回一個Promise版本的readFile函數。Promise提供then方法加載回調函數,catch方法捕捉執行過程當中拋出的錯誤。
能夠看到,Promise的寫法只是回調函數的改進,使用then方法之後,異步任務的兩段執行看着更清楚了,除此以外,並沒有新意。
Promise有一個問題是代碼冗餘,原來的任務被Promise包裝了一下,無論怎麼操做,看上去的一堆then方法,原來的語義變得很不清楚了。
7. Generator函數處理異步
協程
傳統的編程語言,早有異步編程的解決方案(多任務解決方案)。其中有一種叫「協程(coroutine)」,意思是:多個線程協做完成任務。協程有點像函數,又有點像線程。它的運行流程以下:
第一步:協程A開始執行
第二步:協程A執行到一半,進入暫停狀態,執行權交轉移到協程B
第三步:(一段時間後)協程B交換執行權
第四步:協程B恢復執行
舉例來講,讀取文件的協程寫法以下:
function* asyncJob() { // ...其餘代碼 var f = yield readFile(fileA); // ...其餘代碼 }
上面代碼中asyncJob是一個協程,它的關鍵就在yield表達式。yield命令表示執行到此處,將執行權交給其餘的協程。也就是說,yield命令是異步任務的兩個階段的分界線。協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續日後執行。它的最大的優勢,就是代碼的寫法很是像同步操做,若是去除yield命令,就是如出一轍的。
協程的Generator函數實現
Generator函數是協程在ES6的實現,最大特色就是能夠交出函數的執行權(暫停執行)。整個Generator函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操做須要暫停的地方,都用yield表達式註明。Generator函數的執行方法以下:
function* gen (x) { var y = yield x + 2; return y; } var g = gen(1); // 調用Generator函數,傳入參數1,返回指針 console.log(g.next()); // 移動指針,直至遇到yield表達式,返回{value: 3, done: false} console.log(g.next(2)); // 移動指針,傳入參數2,做爲上一次yield表達式的返回值,賦給y,返回{value: 2, done: true} console.log(g.next(2)); // 返回{value: undefined, done: true}
執行結果以下:
1. 定義Generator函數gen,function後面有星花「*」,內部有yield表達式
2. 調用Generator,傳入參數1,返回一個內部狀態的指針對象g
3. 第一次調用Generator對象g的next方法,Generator函數開始執行,知道遇到第一個yield表達式,返回yield表達式的值value是3,當前狀態done的值false
4. 第二次調用Generator對象g的next方法,Generator函數從上次yield表達式停下的地方往下執行,遇到return語句,返回return表達式的值value是2(傳入參數的值是2,上次yield表達式的值就是2,而不是x + 2 = 3)
5. 第三次調用Generator對象g的next方法,Generator函數上內上次執行的是return語句,不會再往下執行,返回yield表達式的值value是undefined,當前狀態是done不變
上面代碼中,調用Generator函數,會返回一個內部指針(即遍歷器)g。這是Generator函數不一樣於普通函數的,即執行它不會返回內部語句的return語句的結果,而是一個內部狀態的指針對象。調用指針g的next方法,會移動內部指針(即執行異步任務的第一階段),執行內部語句,只到遇到第一個yield語句,上面是x + 2。
next方法的做用是分階段執行Generator函數。每次調用next方法,會返回一個對象,表示當前階段的信息(value屬性和done屬性)。value屬性是yield語句後面表達式的值,表示當前階段的值;done屬性是一個布爾值,表示Generator函數是否執行完畢,便是否還有下一個階段。
Generator函數的數據交換和錯誤處理
Generator函數能夠暫停執行和恢復執行,這是它能封裝異步任務的根本緣由。此外,它還有兩個特性是它能夠做爲異步編程的完整解決方案:函數內外的數據交換和錯誤處理機制。
next返回值的value屬性,是Generator函數向外部輸出的數據,next方法還能夠接受參數,向Generator函數體內輸入數據。
function* gen(x){ var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next(2) // { value: 2, done: true }
上面代碼中,第一個next方法的value屬性,返回表達式x + 2的值3。第二個next方法帶有參數2,這個參數能夠傳入Generator函數內部,做爲上顎階段任務的返回結果,被函數體內部的變量y接收。所以,這一步的value屬性,返回的就是2(變量y的值) 。
Generator函數內部還能夠部署錯誤處理代碼,不作函數體外拋出的錯誤。
function* gen (x) { try { var y = yield x + 2; } catch (e) { console.log(e); } return y; } var g = gen(1); // 調用Generator函數,傳入參數1,返回指針 console.log(g.next()); // 指針移動,直至遇到yield表達式,返回value:3, done:false, 這是由於後面還有一個return console.log(g.throw('出錯了!!!!')); // {value: undefined, done: true} 使用指針對象的throw方法拋出錯誤,在函數體內被catch捕獲並傳遞錯誤信息,輸出錯誤信息,返回done:true,沒有value屬性 console.log(g.next()) // {value: undefined, done: true}
執行結果以下:
1. 定義Generator函數,function後面有星花,函數內部有yield表達式
2. 調用Generator函數gen,返回指向內部狀態的指針對象g
3. 第一次調用對象g的next方法,指針移動,只到執行到第一個yield表達式,返回{value: 3, done: false}
4. 調用對象g的throw方法,拋出錯誤,函數內部的try...catch捕獲錯誤,輸出錯誤,返回結果{value: undefined, done: true}。
5. 第二次調用對象g的next方法,輸出{value: undefined, done: true}
上面代碼在Generator函數體外,使用指針對象的throw方法拋出的錯誤,能夠被函數體內的try...catch代碼塊捕獲。這意味着,出錯的代碼與處理錯誤的代碼分離開了,這對異步編程很重要。
異步任務的封裝
看下面的代碼如何使用Generator函數執行一個異步任務
var fetch = require('node-fetch'); function* gen () { var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result); } var g = gen(); // 執行Generator函數,獲取指針 var result = g.next(); // 移動指針,執行函數,直至遇到yield表達式fetch(url),它執行的是異步操做 result.value.then(function (data) { // fetch返回的是一個Promise對象,所以要用then方法調用下一個next方法 console.log(data) }).then(function (data) { g.next(); })
上面代碼中,Generator函數封裝了一個異步操做,該操做先讀取一個遠程接口,而後從JSON格式的數據分析信息。執行Generator函數以前,先獲取遍歷對象,而後用next方法執行,執行異步任務的第一階段。因爲Fetch模塊返回的是一個Promise兌現給,所以要用then方法調用下一個next方法。
Thunk
Thunk函數是自動執行Generator函數的一種方法。Thunk函數在編程語言剛剛起步的時候被提出,即求值策略,函數的參數到底應該在何時求值問題。
Thunk函數的含義
編譯器的「傳名調用」實現,每每是將參數放到一個臨時函數中,再將這個臨時函數傳入函數體。這個臨時函數就叫作 Thunk函數。
let x = 1;
function f(m) { return m * 2; } console.log(f(x + 5)); // 等同於 let x = 1; var thunk = function () { return x + 5; } function f(thunk) { return thunk() * 2; } console.log(f(thunk))
輸出結果以下:
上面的代碼中,先定義函數f,調用函數的時候傳入表達式x + 5,那這個參數何時替換成6呢?一種方式是「傳值調用」 ,即在進入函數體以前,就計算x + 5的值(等於6),再將值傳入函數f。C語言就是採用這種方式。
另外一種是「傳名調用」,即將表達式x + 5傳入函數體,只在用到它的時候求值。Haskell語言t採用這種策略。
兩種方式哪種更好呢?回答是各有利弊。傳值調用比較簡單,可是對參數求值的時候尚未用到這個參數,可能形成性能損失。
編譯器的「傳名調用」實現,每每是將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體。這個臨時函數就叫作「Thunk函數」。
JavaScript語言中的Thunk函數
JavaScript語言是傳值調用,它的Thunk函數含義全部不一樣。在JavaScript語言中Thunk函數替換的不是表達式,而是多參數函數,將其替換成一個只接受回調函數做爲參數的單參數函數。
// 正常版本的readFile(多參數版本) fs.readFile(fileName, callback); // Thunk版本的readFile(單參數版本) var Thunk = function (fileName) { return function (callback) { return fs.readFile(fileName, callback) } } var readFileThunk = Thunk(fileName); readFileThunk();
上面代碼中,fs模塊的readFile方法是一個多參數函數,兩個參數分別爲文件名和回調函數。通過轉換器處理,它變成一個單參數函數,只接受回調函數做爲參數。這個單參數版本,就叫作Thunk函數。
任何函數,只要參數有回調函數,就可以改寫成Thunk函數的形式。下面是個Thunk函數轉換器。
//ES5版本 var Thunk = function (fn) { return function () { var args = Array.prototype.slice.call(arguments); // 使用call方法改變slice函數運行上下文,arguments雖然不是數組,slice(0, end)獲得參數數組 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); } } }
ES5版本
1. 定義Thunk函數,傳入參數fn
2. Thunk函數體內返回一個函數對象
3. 在函數內,使用Array.property.slice.call方法獲取外層函數的參數,返回數組,放在args中
4. 在函數內,再返回一個函數,把callback放在參數數組args的末尾
5. 最後在參數上使用apply方法調用,上下文環境爲當前Thunk對象,傳入參數爲數組args
ES6版本
1. 定義Thunk函數,傳入參數fn
2. Thunk函數體內返回一個函數對象,參數使用擴展運算符...將參數args轉爲都好分割的參數序列
3. 在函數體內,再返回一個函數,傳入參數爲callback
4. 最後使用call方法,在當前對象上調用fn方法,傳入參數爲args分隔號的參數序列和callback
使用上面的轉換器,生成fs.readFile的Thunk函數
var readFileThunk = Thunk(fs.readFile); readFileThunk(fileA)(callback);
下面是一個完整的例子
function f (a, cb) { cb(a); } const ft = Thunk(f); ft(1)(console.log) // 輸出1
1. 定義一個有兩個參數的函數f,第二個參數是回調函數,在函數體內調用回調函數並傳入第一個參數
2. 調用Thunk轉換器,傳入參數f,f被轉換成Thunk函數ft
3. 調用ft,傳入參數1,返回的是內部函數,再傳入參數console.log,最後在1上執行console.log(1),輸出1
看上去是先傳入參數獲得一個函數,而後當即執行這個函數並傳入回調函數做爲參數,只是在調用的時候減小了參數。
Thunkify模塊
在生產環境,可使用Thunkify模塊,使用命令npm install thunkify安裝Thunkify模塊。使用方式以下:
var thunkify = require('thunkify'); var fs = require('fs'); var read = thunkify(fs.readFile); read('package.json')(function (err, str) { // 回調函數的函數體 })
Thunkif的源碼和上面的轉換很像
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); } } } };
在代碼裏多了一個檢查機制,變量called確保回調函數只運行一次。這樣設計與下文的Generator函數相關。
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值容許回到函數執行一次,因此只輸出一行結果。
Generator函數的流程管理
Thunk函數有什麼用呢? 只是爲了減小參數嗎?在ES6中有了Generator函數,Thunk函數能夠用於Generator函數的自動流程管理。Generator函數能夠自動執行。
function* gen() { // ... } var g = gen(); var res = g.next(); while(!res.done){ console.log(res.value); res = g.next(); }
上面代碼中,Generator函數gen會自動執行完全部步驟。
可是這不適合異步操做。若是必須保證前一步執行完,才能執行後一步,上面的自動執行就不可取。這時Thunk函數就能排上用場。以讀取文件爲例,下面的Generator函數封裝了兩個異步操做。
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()); } var g = gen(); var r1 = g.next(); r1.value(function (err, data) { if (err) throw err; var r2 = g.next(data); r2.value(function (err, data) { if (err) throw err; g.next(data); }) })
上面代碼中,使用yield命令將程序的執行權移除Generator函數,那麼就須要一種方法再將執行權交還給Generator函數。手動執行指針對象g是Generator函數的內部指針,表示目前執行到哪一步。next方法負責將指針移動到下一步,並返回當前這一步的信息(即yield表達式的值,包含value屬性和done屬性)。這裏的自動執行步驟實際上是返回調用g.next()方法。下面咱們將探討如何用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 () { yield 1; yield 2; return 3; } run(g)
上面代碼的run函數,就是一個 Generator 函數的自動執行器。內部的next函數就是 Thunk 的回調函數。next函數先將指針移到 Generator 函數的下一步(gen.next方法),而後判斷 Generator 函數是否結束(result.done屬性),若是沒結束,就將next函數再傳入 Thunk 函數(result.value屬性),不然就直接退出。
有了這個執行器,執行 Generator 函數方便多了。無論內部有多少個異步操做,直接把 Generator 函數傳入run函數便可。固然,前提是每個異步操做,都要是 Thunk 函數,也就是說,跟在yield命令後面的必須是 Thunk 函數。
var g = function* () { var f1 = yield readFileThunk('fileA'); var f2 = yield readFileThunk('fileB'); var fn = yield readFileThunk('fileN'); }; run(g);
上面代碼中,函數g封裝了n個異步讀取文件操做,只要執行run函數,這些操做就會自動完成。這樣異步操做不只能夠寫的像同步函數,並且一行代碼就能夠所有執行。
Thunk函數不是Generator函數自動執行的惟一方法。自動執行的關鍵是,必須有一種機制,自動控制Generator函數的流程,接收和交還程序的執行權。回調函數能夠作這一點,Promise也能夠作到。
co模塊
co模塊是著名程序員TJ Holowaychuk於2013年6月發佈的一個小工具,用於Generator函數的自動執行。
下面是一個Generator函數,用於以此讀取兩個文件。
var readFile = require('fs-readfile-promise'); var gen = function *() { var f1 = yield readFile('./a.txt'); var f2 = yield readFile('./b.txt'); console.log(f1); console.log(f2); } var co = require('co'); co(gen).then(function () { console.log('Generator函數執行完成') });
結果以下
注:這裏是在node.js環境下才能執行,在html頁面中不能執行上面的代碼。
上面代碼中,Generator函數只要傳入co函數中,就會自動執行。co函數返回一個Promise對象,所以能夠用then方法添加回調函數。
co模塊的原理
co爲何能夠自動執行Generator函數呢?前面說過,Generator函數就是一個異步操做的容器。它的自動執行須要一種機制,當異步操做有告終果,就能自動交會執行權。
有兩種該方法能夠作到這一點
(1) 回調函數,將異步操做包裝成Thunk函數,在回調函數裏交回執行權。
(2) Promise對象,將異步操做包裝成Promise對象,用then方法交回執行權。
co模塊其實就是將兩種自動執行器(Thunk函數和Promise對象),包裝成一個模塊。使用co的前提條件是,Generator函數的yield命令後面,必定是Thunk函數或者Promise對象。若是數組或對象的成員,所有都是Promise對象,也可使用co。
基於Promise對象的自動執行
上面介紹了Thunk函數的自動執行器,下面來看基於Promise對象的自動執行器,這是理解co模塊必須的。繼續使用上面的例子。首先把fs模塊的readFile方法包裝成一個Promise對象。
var fs = require('fs'); var readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function (error, data) { if (error) return reject(error); resolve(data); }) }) } var gen = function *() { var f1 = yield readFile('./a.txt'); var f2 = yield readFile('./b.txt'); console.log(f1.toString()) console.log(f2.toString()) }; var g = gen(); g.next().value.then(function (data) { g.next(data).value.then(function (data) { g.next(data) }) })
上面代碼中手動執行Generator函數,執行結果以下:
手動執行其實就是用then方法,層層添加回調函數。知道這一點就能夠寫一個自動執行器。
function run(gen) { var g = gen(); function next(data) { var result = g.next(data); if (result.done) return result.value; result.value.then(function (data) { next(data); }); } next(); } run(gen);
自動執行器執行結果和上面是同樣的。上面代碼中,只要Generator函數尚未執行到最後一步,next函數就調用自身,以此實現自動執行。
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); }); }
接着,co將Generator函數的內部指針對象的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函數,內部一共只有四行命令:
1. 根據ret的done屬性檢查當前是否爲Generator函數的最後一步,若是是就返回。
2. 確保每一步的返回值,都是Promise對象。
3. 使用then方法,爲返回值加上回調函數,而後經過onFulfilled函數再次調用next函數。
4. 在參數不符合要求的狀況下(參數非Thunk函數和Promise對象),將Promise對象的狀態改成rejected,從而終止執行。
處理併發的異步操做
co支持併發的異步操做,即容許某些操做同時進行,等到他們所有完成,才進行下一步。要把併發的操做都放在數組對象裏面,跟在yield語句後面。
// 數組的寫法 co(function* () { var res = yield [ Promise.resolve(1), Promise.resolve(2) ]; console.log(res); }).catch(onerror); // 對象的寫法 co(function* () { var res = yield { 1: Promise.resolve(1), 2: Promise.resolve(2), }; console.log(res); }).catch(onerror);
下面是另外一個例子
co(function* () { var values = [n1, n2, n3]; yield values.map(somethingAsync); }); function* somethingAsync(x) { // do something async return y }
上面的代碼,容許併發三個somethingAsync異步操做,等到他們所有完成,纔會進行下一步。
處理Stream
Node提供Stream(流媒體)模式讀寫數據,特色是一次只處理數據的一部分,數據分紅一塊塊以此處理,就像「數據流」同樣。這樣對於處理大規模數據很是有利。Steam模式使用EventEmitter API,會釋放三個事件:
data事件:下一塊數據已經準備好了。
end事件:整個「數據流」處理完了。
error事件:發生錯誤。
使用Promise.race()函數,能夠判斷這三個事件之一誰先發生,只有當data事件最早發生時,才進入下一個數據塊的處理。從而咱們能夠經過一個while循環,完成全部數據的讀取。
const co = require('co'); const fs = require('fs'); const stream = fs.createReadStream('./Les_Miserables.txt'); let valjeanCount = 0; co(function *() { while (true) { const res = yield Promise.race([ new Promise(resolve => stream.once('data', resolve)), new Promise(resolve => stream.once('end', resolve)), new Promise((resolve, reject) => stream.once('error', reject)) ]); if (!res) { break; } stream.removeAllListeners('data'); stream.removeAllListeners('end'); stream.removeAllListeners('error'); valjeanCount += (res.toString().match(/valjean/ig) || []).length; } console.log('count:', valjeanCount) });
執行結果以下:
上面代碼採用Stream模式讀取Les_Miserables.txt這個文件,對於每一個數據塊都用stream.once方法,在data,end,error三個事件上添加一次性回調函數。變量res只有在data事件發生時纔有值,而後累加每一個數據塊之中「valjean」這個單詞,能夠看到Les_Miserables.text這個文件中「valjean」這個單詞一共出現了1153次。
8. async函數
8.1 含義
ES2017標準中引入了async函數,使得異步操做變得更加方便。async是Generator函數的語法糖。下面有一個Generator函數,一次讀取兩個文件。
/** * Generator函數,依次讀取兩個文件 */ const fs = require('fs'); const readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function (error, data) { if (error) return reject(error) resolve(data) }) }) } const gen = function *() { const f1 = yield readFile('./a.txt') const f2 = yield readFile('./b.txt') console.log(f1.toString()) console.log(f2.toString()) } function run(gen) { var g = gen(); function next(data) { var result = g.next(data); if (result.done) return result.value; result.value.then(function (data) { next(data); }); } next(); } run(gen)
讀取結果以下:
改寫成async函數,以下:
const fs = require('fs'); const readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function (error, data) { if (error) return reject(error) resolve(data) }) }) } /** * async函數實現讀取兩個文件 * @returns {Promise<void>} */ const asyncReadFile = async function () { const f1 = await readFile('./a.txt') const f2 = await readFile('./b.txt') console.log(f1.toString()) console.log(f2.toString()) } asyncReadFile()
執行結果以下:
他們執行的結果是同樣的,比較一下能夠看出,async函數其實就是將Generator函數的星號(*)替換成async並放在function關鍵字的前面,函數體內用await代替了yield關鍵字,僅此而已。async函數對Generator函數的改進有四點:
(1)內置執行器
Generator函數的執行必須依靠執行器,因此纔有了co模塊,而async函數自帶執行器。也就是說async函數的執行,和普通函數同樣,只要調用就好,只要一行。上面代碼中asyncReadFile()這一句就能夠自動執行async函數。這徹底不像Generator函數,須要調用next方法,或者co模塊,才能真正執行,獲得最後結果。
(2)更好的語義
async和await,比起星號和yield,語義更加清楚了。async表示函數裏有異步操做,await表示緊跟在後邊的表達式須要等待結果。
(3)更廣的適用性
co模塊約定,yield命令後面只能是Thunk函數或者Promise對象,而async函數的await命令後面,可使Promise對象和原始的類型的值(數值,字符串,布爾值,可是這是等同於同步操做)
(4)返回的是Promise
async函數返回的是Promise對象,這比Generator函數的返回值是Iterator對象方便多了。能夠用then方法指定下一步操做。
進一步說,async函數徹底能夠看作是多個異步操做,包裝成一個Promise對象,而await命令就是內部then命令的語法糖。
8.2 基本用法
async函數返回一個Promsie對象(嗯嗯,都是返回Promise對象),可使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到異步操做完成,再接着執行函數體內後面的語句。
下面是一個例子:
async function getStockPriceByName(name) { var symbol = await getStockSymbol(name); var stockPrice = await getStockPrice(symbol); return stockPrice; } getStockPriceByName('goog').then(function (result) { console.log(result); });
上面代碼是一個獲取股票的函數,函數前面的async關鍵字表示這個函數內部有異步操做。調用該函數時,會當即返回一個Promise對象。
下面是另一個例子,指定多少毫秒以後輸出一個值。
function timeout(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }) } async function asyncPrint(value, ms) { await timeout(ms); console.log(value) } asyncPrint('hello world', 50);
1. 定義timeout函數,返回一個Promise對象,在ms毫秒以後將Promise對象的狀態改成fullfiled狀態
2. 定義一個async(Generator)函數,async至關於星花,await至關於yield表達式,await表達式後面調用timeout函數,傳入參數ms
3. 調用async函數,傳入兩個參數「hello world」, 50
4. 調用async函數,它自帶執行器,遇到await表達式,執行後面的timeout方法,傳入參數ms;timeout方法返回一個Promise對象,指定ms毫秒以後執行resolve,將Promise對象的狀態修改成fullfiled
5. async方法中偵測到timeout方法執行完畢以後再執行後面的console.log方法,輸出「hello world」,至關於延遲了50毫秒再執行後面的console.log(value)
因爲async函數返回的是Promise對象,能夠做爲await命令的參數。因此上面的代碼也能夠寫成下面的形式
async function timeout(ms) { await new Promise((resolve) => { setTimeout(resolve, ms); }); } async function asyncPrint(value, ms) { await timeout(ms); console.log(value); } asyncPrint('hello world', 50);
async函數有多種使用形式
// 函數聲明 async function foo() { } // 函數表達式 const foo = async function () { } // 對象的方法 let obj = {async foo()}; obj.foo().then(); // class的方法 class Storage { constructor() { this.cachePromise = caches.open('avatars'); } async getAvatar(name) { const cache = await this.cachePromise; return cache.match(`/avatars/${name}.jpg`) } } const storage = new Storage(); storage.getAvatar('jake').then(...) // 箭頭函數 const foo = async () => {}
8.3 語法
返回Promise對象
async函數返回一個Promise對象。async函數內部return語句返回的值(done屬性爲false時,value的值),會成爲then方法回調函數的參數。
async function f() { await 'hello' return 'world' } f().then(v => console.log(v))
1. 定義async(Generator)函數,async至關於星花,await至關於yield表達式;return語句後面的返回值會變成返回的Promise對象的then語句的參數
2. 調用async方法,自帶執行器,獲得一個Promise對象,調用它的then方法,傳入的參數爲async方法return語句的值,打印這個值,最後輸出「world」
輸出結果以下:
上面代碼中,函數f內部return命令的返回值,會被then方法回調函數接收到。
async函數內部冒出的錯誤,會致使返回的Promise對象爲reject狀態。拋出的錯誤對象會被catch方法回調函數接收到。
async function f() { throw new Error('出錯了') } f().then( value => console.log(value), error => console.log(error) )
輸出結果以下:
Promise對象的狀態變化
async函數返回的Promise對象,必須等到內部全部的await命令後面的Promise對象執行完,纔會發生狀態改變,除非遇到return語句或者拋出錯誤。也就是說,只有async函數內部的異步操做執行完,纔會執行then方法指定的回調函數。這一點和Generator函數是不同的,Generator函數是調用指針對象的next方法纔會往下執行。
下面看一個例子:
async function getTitle (url) { let response = await fetch(url); let html = await response.text(); return html.match(/<title>([\s\S]+)<\/title>/i)[1]; } getTitle('https://tc39.github.io/ecma262/').then(console.log);
1. 定義async函數(Generator函數)getTitle,async至關於星花,內部await命令至關於yield命令。
2. await命令後面調用fetch方法,抓取網頁,給response賦值
3. await命令後面調用response.text方法,給html賦值
4. return語句返回匹配的標題
5. 調用getTitle函數,只有async函數內部全部的await命令執行完畢纔會調用then方法並將return語句後面的值傳遞給then方法做爲參數,最終打印參數
在網頁中的執行結果以下:
函數getTitle內部有三個操做,抓取網頁,取出文本,匹配頁面標題。只有這三根操做所有完成纔會執行then方法中的console.log。這裏then方法中名沒有傳入參數,可是console.log方法卻直接拿到返回值做爲getTitle方法的返回值做爲參數輸出。
await命令
一般,await命令後面是一個Promise對象,返回該對象的結果。若是不是Promise對象,就直接返回對應的值。
async function f () { return await 123; } f().then(v => console.log(v))
上面代碼中,await命令的參數值是123,這等同於return 123。調用async方法也會等到awa命令執行完,返回123,最後打印123。
另外一種狀況,await命令後面是一個thenable對象(即定義then方法的對象),那麼await會將其等同於Promise對象。
class Sleep { constructor (timeout) { this.timeout = timeout } then (resolve, reject) { const startTime = Date.now() setTimeout( () => resolve(Date.now() - startTime), this.timeout ) } } (async () => { const actualTime = await new Sleep(1000) console.log(actualTime) })();
輸出結果以下:
上面代碼中,await命令後面是一個Sleep對象的實例。這個實例不是Promise對象,可是由於定義了then方法,await會將其視爲Promise處理。
await命令後面的Promise對象若是變成reject狀態,則reject的參數會被catch方法的回調函數接收到。
async function f () { await Promise.reject('出錯了') } f().then(v => console.log(v)).catch(e => console.log(e))
注意,上面代碼中await語句前面沒有return,可是reject方法的參數依然傳入了catch方法的回調函數。這裏若是在await前面加上return,則效果是同樣的。
任何一個await語句後面的Promise對象變成reject狀態,那麼整個async函數都會中斷執行。
async function f() { await Promise.reject('出錯了'); await Promise.resolve('hello world'); // 不會執行 }
上面代碼,第二個await語句時不會執行的,由於第一個await語句變成了reject。下面會介紹處理這個reject狀態並繼續往下執行的方法。
有時候咱們但願及時前一個操做失敗,也不要終端後面的異步操做。這時能夠將第一個await放在try...catch裏面,這樣無論這個異步操做是否成功,第二個await都會執行。
async function f () { try { await Promise.reject('出錯了'); } catch (e) { } return await Promise.resolve('hello world'); } f().then(v => console.log(v))
執行結果以下:
上面代碼try代碼塊中await Promise.reject('出錯了')語句雖然會拋出錯誤,可是在catch語句塊中沒有處理,而後繼續執行return await Promise.resolve('hello world'),最後輸出‘hello world’。
另外一種方法是await後面的Promise對象再跟一個catch方法,處理前面可能出現的錯誤。
async function f () { await Promise.reject('出錯了') .catch(e => console.log(e)); return await Promise.resolve('hello world'); } f().then(v => console.log(v))
輸出結果以下:
在reject方法後面直接catch方法,處理內部錯誤。外部的then方法處理正常狀況,這樣既能夠處理內部的錯誤,也能夠處理外部的錯誤。
錯誤處理
若是await後面的異步操做出錯,那麼等同於async函數返回的Promise對象被reject。
async function f () { await new Promise(function (resolve, reject) { throw new Error('出錯了') }) } f().then(v => console.log(v)).catch(e => console.log(e))
執行結果以下:
上面代碼中,async函數f執行後,await後面的Promise對象會拋出一個錯誤對象,致使catch方法的回調函數被調用,它的參數就是拋出的錯誤對象。
防止錯誤拋出的方法,也是將其放在try...catch代碼塊中,吃掉錯誤。
async function f () { try { await new Promise(function (resolve, reject) { throw new Error('出錯了') }) } catch (e) { } return await ('hello world') } f().then(value => console.log(value)).catch(e => console.log(e))
輸出結果以下:
若是有多個await命令,能夠統一放在try...catch結構中。
async function main () { try { const val1 = await firstStep(); const val2 = await secondStep(val1); const val3 = await thirdStep(val1, val2); console.log('final: ' val3); } catch (e) { console.log(e) } }
下面的例子,使用try...catch解構,實現多3次重複嘗試。
const superagent = require('superagent'); const NUM_RETRIES = 3; async function test () { let i; for (i = 0; i < NUM_RETRIES; i++) { try { await superagent.get('http://google.com/this-throws-an-error'); break } catch (e) { } } console.log(i) } test();
輸出結果以下:
上面代碼中,若是await操做成功,就會使用break語句退出循環;不然會被catch語句捕捉,進入下一輪循環。
使用注意點
第一點,前面已經說過,await命令後面的Promise對象,運行結果多是rejected,因此最好把await命令放在try...catche代碼塊中。
async function myFunction() { try { await somethingThatReturnsAPromise(); } catch (err) { console.log(err); } } // 另外一種寫法 async function myFunction() { await somethingThatReturnsAPromise() .catch(function (err) { console.log(err); }); }
第二點,多個await命令後面的異步操做,若是不存在繼發關係,最好讓他們同時觸發。
let foo = await getFoo();
let bar = await getBar();
上面代碼中,getFoo,getBar是連個獨立的異步操做(相互不依賴),被寫成繼發關係。這樣比較耗時,由於只有getFoo完成之後,纔會執行getBar,徹底可讓他們同時觸發。
let foo = await getFoo(); let bar = await getBar(); // 寫法一 let [foo, bar] = await Promise.all([getFoo(), getBar()]); // 寫法二,異步方法同步執行 let fooPromise = getFoo(); let barPromise = getBar(); let foo = await fooPromise; let bar = await barPromise;
第三點, await命令只能用在async函數中,若是用在普通函數中,就會報錯。
async function dbFuc(db) { let docs = [{}, {}, {}]; // 報錯 docs.forEach(function (doc) { await db.post(doc); }); }
上面代碼會報錯,由於await用在普通函數中。可是若是將forEach方法的參數改爲async也是有問題的。
function dbFuc(db) { //這裏不須要 async let docs = [{}, {}, {}]; // 可能獲得錯誤結果 docs.forEach(async function (doc) { await db.post(doc); }); }
上面代碼不會正常工做,緣由是這時三個db.post操做是併發執行的,也是同步執行,而不是繼發執行。正確的寫法是使用for...of循環。
async function dbFuc(db) { let docs = [{}, {}, {}]; for (let doc of docs) { await db.post(doc); } }
若是確實但願多個請求併發執行,可使用Promise.all方法。當三個請求都會resolved的時候,下面的兩種寫法效果相同。
async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = await Promise.all(promises); console.log(results); } // 或者使用下面的寫法 async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = []; for (let promise of promises) { results.push(await promise); } console.log(results); }
目前,esm模塊加載器支持頂層await,即await命令能夠不放在async函數裏面,直接使用。
// async 函數的寫法 const start = async () => { const res = await fetch('google.com'); return res.text(); }; start().then(console.log); // 頂層 await 的寫法 const res = await fetch('google.com'); console.log(await res.text());
上面代碼中,第二種寫法的腳本必須使用esm加載器,纔會生效。
第四點,async函數能夠保留運行堆棧。
const a = () => { b().then(() => c()); };
上面代碼中給,函數a內部運行了一個異步任務b()。當b()運行的時候,函數a()不會中斷,而是繼續執行。等到b()運行結束,可能a()早已經雲心剛結束了。b()所在的上下文環境已經消失。若是b()或者c()報錯,錯誤堆棧將不包括a().
將這個例子改造一下以下:
const a = async () => {
await b();
c();
};
上面代碼中,b()運行的時候,a()是暫停執行,上下文環境保存着,一旦b()或c()報錯,錯誤堆棧將包括a()。
8.4 async函數的實現原理
async函數的實現原理,就是將Generator函數和自動執行器,包裝在一個函數裏。
async function fn(args) { // ... } // 等同於 function fn(args) { return spawn(function* () { // ... }); }
全部的async函數均可以寫成上面的第二種形式,其中的spawn函數就是自動執行器。下面給出spaw函數的實現,基本就是前文自動執行器的翻版。
function spawn (genF) { return new Promise(function (resolve, reject) { const gen = genF() function step (nextF) { let next; try { newxt = nextF() } catch (e) { return reject(e) } if (next.done) { return resolve(next.value) } Promise.resolve(next.value).then(function (v) { step(function () { return gen.next(v) }) }, function (e) { step(function () { return gen.throw(e) }) }) } step(function () { return gen.next(undefined) }) }) }
8.5 與其餘異步處理方法的比較
咱們經過一個例子來看看async與Promise,Generator函數的比較。
首先是Promise的寫法:
function chainAnimationsPromise (elem, animations) { // 變量ret用來保存上一個動畫的返回值 let ret = null // 新建一個空的Promise let p = Promise.resolve() // 使用then方法,添加全部動畫 for (let anim of animations) { p = p.then(function (val) { ret = val return anim(elem) }) } // 返回一個部署了錯誤捕捉機制的Promise return p.catch(function (e) { // 忽略錯誤,繼續執行 }).then(function () { return ret }) }
雖然Promise的寫法比回調函數的寫法大大改進,可是一眼看上去,代碼徹底都是Promise的API(then,catch),操做自己的語義反而不容易看出來。
接着是Generator函數的寫法。
function chainAnimationsGenerator (elem, animations) { return spawn(function *() { let ret = null try { for (let anim of animations) { ret = yield anim(elem) } } catch (e) { // } return ret }) }
上面代碼使用Generator函數遍歷了每一個動畫,語義比Promise寫法更加清晰,用戶定義的操做所有都出如今spawn函數的內部。這個寫法問題在於,必須有一個任務運行器,自動執行Generator函數,上面代碼中spawn函數就是自動執行器,它返回一個Promise對象,並且必須保證yield語句後面的表達式,必須返回一個Promise。
最後是yield函數的寫法。
async function chianAnimationsAsync(elem, animations) { let ret = null try { for (let anim of animations) { ret = await anim(elem) } } catch (e) { // 忽略錯誤,繼續執行 } return ret }
能夠看到async函數的實現最簡潔,最符合語義,幾乎沒有語義不想管的代碼。它將Generator寫法中的的自動執行器,改在語言層面提供,不暴露給用戶,所以代碼量最少。若是使用Generator寫法,自動執行器須要用戶本身提供。
8.6 按順序完成異步操做
實際開發中,常常遇到一組異步操做,須要按順序執行。好比讀取一組URL地址,而後按照讀取順序輸出結果。
Promise的寫法以下:
function logInOrder (urls) { // 遠程讀取全部的URL const textPromises = urls.map(url => { return fetch(url).then(response => response.text()) }) // 按次序輸出 textPromises.reduce((chain, textPromise) => { return chain.then(() => textPromise).then(text => console.log(text)) }, Promise.resolve()) }
上面代碼使用fetch方法,同時遠程讀取一組URL。每一個fetch操做都返回一個Promise對象,放入textPromises數組。而後,reduce方法一次處理每一個Promise對象,放入textPromise數組。而後,reduce方法一次處理每一個Promise對象,而後使用then,將全部Promise對象鏈接起來,所以就能夠以此輸出結果。
這種寫法不直觀,還要使用數組對象的reduce方法,可讀性比較差,下面是async函數的實現。
async function logInOrder (urls) { for (let url of urls) { const response = await fetch(url) console.log(await response.text()) } }
上面代碼大大簡化,問題是全部的遠程操做都是繼發。只有前一個RUL返回結果,纔會去讀下一個URL,這樣的效率不好,浪費時間,咱們須要併發發出遠程請求,結果按照前後順序輸出就行了。
async function loadInOrder (urls) { // 併發讀取遠程URL const textPromises = urls.map(async url => { const response = await fetch(url) return response.text() }) // 按次序輸出 for (const textPromise of textPromises) { console.log(await textPromise) } }
上面代碼中,雖然map方法的參數是async函數,可是他們是併發執行的,由於只有async函數內部是繼發執行,外部不受影響。後面的for...of循環內部使用了await,所以實現了按順序輸出。
8.7 異步遍歷
Iterator接口是一種數據遍歷的協議,只要調用遍歷器對象的next方法,就會獲得一個指針對象,表示當前遍歷指針所在的那個位置的信息。next方法返回的對象和Generator的next方法返回的對象是同樣的{value: '', done: ''},其中value表示當前的數據的值,done是一個布爾值,表示遍歷是否結束。
這裏隱藏着一個規定,next方法必須是同步的,只要調用就必須馬上返回該值。這就是說,一旦執行next方法,就必須同步地獲得value和done這兩個屬性。若是遍歷指針正好指向同步操做,這是沒有問題的,可是對於異步操做,就不太合適了。目前解決的方法是Generator函數裏的異步操做,返回一個Thunk函數或者Promise對象,即value是一個Thunk函數或者Promise對象,等待之後返回真正的值,done屬性則仍是同步產生的。
ES2018引入「異步遍歷器」(Async Iterator),爲異步操做提供原生的遍歷器接口,即value和done屬性都是異步產生的。
異步遍歷的接口
異步遍歷器的最大的語法特色,就是調用遍歷器的next方法,返回的是一個Promise對象。
asyncIterator .next() .then( ({ value, done }) => /* ... */ );
上面示例代碼中asyncIterator是一個異步遍歷器,調用next方法之後,返回一個Promise對象。所以可使用then方法指定,這個Promise對象的狀態變成resolved的回調函數。回調函數的參數以一個有value和done屬性的對象,這個跟同步遍歷器是同樣的。
一個對象的同步遍歷器的接口部署在Symbo.iterator屬性上。一樣的異步遍歷器接口部署在Symbol.asyncIterator屬性上面。無論是什麼樣的對象,只要它的Symbol.asyncIterator屬性有值,就表示能夠對它進行異步遍歷。
下面是一個異步遍歷器的示例代碼:
const asyncIterable = createAsyncIterable(['a', 'b']); const asyncIterator = asyncIterable[Symbol.asyncIterator](); asyncIterator .next() .then(iterResult1 => { console.log(iterResult1); // { value: 'a', done: false } return asyncIterator.next(); }) .then(iterResult2 => { console.log(iterResult2); // { value: 'b', done: false } return asyncIterator.next(); }) .then(iterResult3 => { console.log(iterResult3); // { value: undefined, done: true } });
代碼中,異步遍歷器其實返回了兩次值。第一次調用的時候,返回一個Promise對象,等到Promise對象resolve了,再返回一個表示當前數據成員信息的對象,這就是說,異步遍歷器與同步遍歷器的行爲是一致的,只是會先返回Promise對象做爲中介。
因爲異步遍歷器的next方法,返回的是一個Promise對象。所以,能夠把它放在await命令後面。
async function f() { const asyncIterable = createAsyncIterable(['a', 'b']); const asyncIterator = asyncIterable[Symbol.asyncIterator](); console.log(await asyncIterator.next()); // { value: 'a', done: false } console.log(await asyncIterator.next()); // { value: 'b', done: false } console.log(await asyncIterator.next()); // { value: undefined, done: true } }
上面代碼中,next方法用await處理後,就沒必要使用then方法了。這個流程已經很接近同步處理了。
注意,異步遍歷器的next方法是能夠連續調用的,沒必要等到上一步產生的Promise對象resolve之後再調用。這種狀況下,next方法會累積起來,自動按照每一步的順序運行下去。下面是一個例子,把全部的next方法放在Promise.all方法裏面。
const asyncIterable = createAsyncIterable(['a', 'b']); const asyncIterator = asyncIterable[Symbol.asyncIterator](); const [{value: v1}, {value: v2}] = await Promise.all([ asyncIterator.next(), asyncIterator.next() ]); console.log(v1, v2); // a b
另一種方法是一次性調用全部的next方法,而後await最後一步操做。
async function runner() { const writer = openFile('someFile.txt'); writer.next('hello'); writer.next('world'); await writer.return(); } runner();
for await of
上面說過,for...of循環用於遍歷同步的Iterator接口。新引入的for await ... of循環,則是調用遍歷異步的Iteator接口。
async function f() { for await (const x of createAsyncIterable(['a', 'b'])) { console.log(x); } } // a // b
上面代碼中,creatAsyncIterator()返回一個擁有異步遍歷器接口的對象,for...of循環自動調用這個對象的異步遍歷器的next方法,會獲得一個Promise對象。await用來處理這個Promise對象,一旦resolve,就會把獲得的值x傳入for....of循環體。
for await...of循環的一個用途,是部署了asyncIterable操做的異步操做,能夠直接放在這個循環體裏。
let body = ''; async function f() { for await(const data of req) body += data; const parsed = JSON.parse(body); console.log('got', parsed); }
上面代碼中,req是一個asyncIterable對象,用來異步讀取數據。能夠看到,使用for await...of循環後,代碼很是簡潔。
若是next方法返回的Promise對象被reject,for await...of就會報錯,要用try...catch捕獲。
async function () { try { for await (const x of createRejectingIterable()) { console.log(x); } } catch (e) { console.error(e); } }
注意,for await...of循環也能夠用於同步遍歷器。
(async function () { for await (const x of ['a', 'b']) { console.log(x); } })();
Node v10支持異步遍歷器,node中的Stream模塊就部署了這個接口,下面是讀取文件的傳統寫法和異步遍歷器的寫法的差別。
// 傳統寫法 function main(inputFilePath) { const readStream = fs.createReadStream( inputFilePath, { encoding: 'utf8', highWaterMark: 1024 } ); readStream.on('data', (chunk) => { console.log('>>> '+chunk); }); readStream.on('end', () => { console.log('### DONE ###'); }); } // 異步遍歷器寫法 async function main(inputFilePath) { const readStream = fs.createReadStream( inputFilePath, { encoding: 'utf8', highWaterMark: 1024 } ); for await (const chunk of readStream) { console.log('>>> '+chunk); } console.log('### DONE ###'); }
異步Generator函數
就像Generator函數返回一個同步遍歷器對象同樣,異步Generator函數的做用,是返回一個異步遍歷器對象。
在語法上,異步Generator函數就是async函數與Generator函數的結合。
async function* gen() { yield 'hello'; } const genObj = gen(); genObj.next().then(x => console.log(x));
代碼輸出結果以下
上面代碼中,gen是一個異步Generator函數,執行後返回一個異步Iterator對象。改對象調用next方法,返回一個Promise對象。
異步遍歷器的設計目的之一,就是Generaotr函數處理同步和異步操做的時候,可以使用同一套接口。
// 同步 Generator 函數 function* map(iterable, func) { const iter = iterable[Symbol.iterator](); while (true) { const {value, done} = iter.next(); if (done) break; yield func(value); } } // 異步 Generator 函數 async function* map(iterable, func) { const iter = iterable[Symbol.asyncIterator](); while (true) { const {value, done} = await iter.next(); if (done) break; yield func(value); } }
上面代碼中,map是一個Generator函數,第一個參數是可遍歷對象iterator,第二個參數是一個回調函數func。map的做用是將iterator每一步返回的值,用func進行處理。上面有兩個版本的map,前一個處理同步遍歷器,後一個處理異步遍歷器。能夠看到連個版本的寫法基本一致。
下面是一個異步Generator函數的例子。
async function* readLines(path) { let file = await fileOpen(path); try { while (!file.EOF) { yield await file.readLine(); } } finally { await file.close(); } }
上面代碼中,異步操做前面使用await關鍵字標明,await後面的操做應該返回Promise對象。凡是shiyongyield關鍵字的地方,就是next方法停下來的地方,它後面的表達式的值(即await file.readLine()的值),會做爲next()返回對象的value屬性,這一點是與同步Generator函數一致的。
異步Generator函數內部,可以同時使用await和yield命令。能夠這樣理解,await命令用於將外部操做產生的值輸入函數內部,yield命令用於將函數內部的值輸出。
上面代碼定義的異步Generator函數的用法以下:
(async function () { for await (const line of readLines(filePath)) { console.log(line); } })()
異步Generator函數能夠與for await...of循環結合起來使用。
async function* prefixLines(asyncIterable) { for await (const line of asyncIterable) { yield '> ' + line; } }
異步Generator函數的返回值是一個異步Iterator,即每次調用它的next方法,會返回一個Promise對象,也就是說,跟在yield命令後面的,應該是一個Promise對象。若是想上面的那個例子那樣,yield命令後面是一個字符串,會被自動包裝成一個Promise對象。
function fetchRandom() { const url = 'https://www.random.org/decimal-fractions/' + '?num=1&dec=10&col=1&format=plain&rnd=new'; return fetch(url); } async function* asyncGenerator() { console.log('Start'); const result = await fetchRandom(); // (A) yield 'Result: ' + await result.text(); // (B) console.log('Done'); } const ag = asyncGenerator(); ag.next().then(({value, done}) => { console.log(value); })
執行順序以下:
1. ag.next()馬上返回一個Promise對象
2. asyncGenerator函數開始執行,打印Start
3. await命令返回一個Promise對象,asyncGenerator函數暫停在這裏
4. A處變成fulfilled狀態,產生的值放入result變量,asyncGenerator函數繼續往下執行
5. 函數在B處的yield暫停執行,一旦yield命令取到值,ag.next()返回的那個Promise對象編程fulfilled狀態
6. ag.next()後面的then方法指定的回調函數開始執行。改回調函數的參數是一個對象{value, done},其中value的值是yield命令後面的那個表達式的值,done的值是false
執行結果以下
A和B兩行的做用相似下面的代碼
return new Promise((resolve, reject) => { fetchRandom() .then(result => result.text()) .then(result => { resolve({ value: 'Result: ' + result, done: false, }); }); });
若是 一部Generator函數拋出錯誤會致使Promise對象的狀態變爲reject,而後拋出的錯誤被catch方法捕獲。
async function* asyncGenerator() { throw new Error('Problem!'); } asyncGenerator().next().catch(err => console.log(err));
執行結果以下:
注意,普通的async函數返回的是一個Promise對象,而異步Generator函數返回的是一個異步Iterator對象。能夠這樣理解,async函數和異步Generator函數,是封裝異步操做的兩種方法,都用來達到同一種目的。區別在於,前者自帶執行器,後者經過for await...of執行,或者能夠本身編寫執行器。下面是一個異步Generator函數的執行器。
// 異步執行器 async function takeAsync(asyncIterable, count = Infinity) { const result = []; const iterator = asyncIterable[Symbol.asyncIterator](); while (result.length < count) { const {value, done} = await iterator.next(); if (done) break; result.push(value); } return result; } // 使用異步執行器 async function f() { async function* gen() { yield 'a'; yield 'b'; yield 'c'; } return await takeAsync(gen()); } f().then(function (result) { console.log(result); // ['a', 'b', 'c'] })
執行結果以下:
上面代碼中,異步Generator函數產生的異步遍歷器,會經過while循環自動執行,每當await iterator.next()完成,就會進入下一輪循環。一旦done屬性變成true,就會調出循環,異步遍歷器執行結束。
異步Generator函數出現之後,JavaScript就有了四種形式的函數:普通函數,async函數,Generator函數,異步Generator函數。一般,若是是一系列按照順序執行的異步操做(好比讀取文件,而後寫入新內容,再存入硬盤)可使用async函數;若是是一系列產生相同數據結構的異步操做(好比一行一行的讀取文件),可使用異步Generator函數。
異步Generator函數也能夠經過next方法的參數,接收外部傳入的數據。
const writer = openFile('someFile.txt'); writer.next('hello'); // 當即執行 writer.next('world'); // 當即執行 await writer.return(); // 等待寫入結束
上面代碼中,openFile是一個異步Generator函數。next方法的參數,向該函數內部的操做傳入數據。每次next方法都是同步執行的,最後的await命令用於等待整個操做結束。
最後,同步的數據結構,也可使用異步Generator函數。
async function* createAsyncIterable(syncIterable) { for (const elem of syncIterable) { yield elem; } }
上面代碼中,因爲沒有異步操做,因此也就沒有使用await關鍵字。
yield * 語句
yield*語句也能夠跟一個異步遍歷器。
async function* gen1() { yield 'a'; yield 'b'; return 2; } async function* gen2() { // result 最終會等於 2 const result = yield* gen1(); } (async function () { for await (const x of gen2()) { console.log(x); } })();
上面代碼中,gen2函數裏的result變量,最後的值是2.
與同步Generator函數同樣,for await...of循環會展開yield*,輸出結果以下: