提示
: 本文是 github 上《Understanding ECMAScript 6》 的筆記整理,程式碼示例也來源於此。大家有時間可以直接讀這本書。雖是英文,但通俗易懂,非常推薦。
前情:
在上一篇文章 你知道為什麼會有 Generator 嗎 裡,我拋磚引玉,介紹了 generator
產生的原因。當時就有夥伴指出 “Generator是用來模擬多執行緒的休眠機制的”、 “Generator執行是惰性的”。那時我就說高階篇裡會有介紹,這裡就好好說一下。
摘要:
這裡的重點,首先是如何與generator
裡通訊,一是用 next()
傳參,二是還可以用 throw()
,不同的是它是往裡拋錯; 其次是有 yield
賦值語句時, generator
內部的執行順序; 最後會是怎麼用同步的方式寫非同步(有可能像 co
哦)。
如果對 generator
不太熟的,可以先看看 這裡
1. 傳參
簡單說就是可以往next
傳引數,而generator
裡 yield
處可以接收到這個引數, 如下例子:
function *createIterator() {
let first = yield 1;
let second = yield first + 2; // 4 + 2
yield second + 3; // 5 + 3
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.next(5)); // "{ value: 8, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
複製程式碼
執行流程
要很明白地解釋上面的執行過程,可藉助這張圖:
顏色相同的是同一次迭代裡執行的,由淺到深,表示迭代的先後順序。如:
- 第一次呼叫
next()
, 執行yield 1
到停止,返回{ value: 1, done: false }
。注意,這時賦值語句let fisrt = ...
沒有執行; - 第二次呼叫
next(4)
, 先將引數4
傳入上一次yield
處,可理解為:
let first = yield 1;
=>
let first = 4;
複製程式碼
再從上次停頓的地方開始執行,就是說先執行賦值語句
let first = 4
複製程式碼
然後執行到下個yield
為止,即
yield first + 2 // 4 + 2
複製程式碼
最後返回 { value: 6, done: false }
之後的 next
依上面的原理而執行,直到迭代完畢。
也就是說,通過next
的引數,generator
產生的 iterator
,與外部環境搭建起了溝通的橋樑,結合 iterator
可以停頓的特點,可以做一些有意思的事,如用同步方式寫回撥等,詳見下文。
2. 往 iterator
裡拋錯
function *createIterator() {
let first = yield 1;
let second = yield first + 2; // yield 4 + 2, 然後丟擲錯誤
yield second + 3; // 不會被執行
}
let iterator = createIterator();
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next(4)); // {value: 6, done: false}
console.log(iterator.throw(new Error("Boom"))); // generator 裡丟擲的錯誤
複製程式碼
根據上面說的執行機制,這裡例子的執行流程可以用這張圖表示:
第三次執行迭代時,我們呼叫 iterator.throw(new Error("Boom"))
, 向 iterator
裡丟擲錯誤,傳入的引數為錯誤資訊。
我們可以改造 createIterator
如下:
function* createIterator() {
let first = yield 1;
let second;
try {
second = yield first + 2;
} catch (ex) {
second = 6;
}
yield second + 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next(4)); // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // "{ value: 9, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
複製程式碼
其執行流程解釋如下:
-
前兩次呼叫
next
情況和上面執行機制裡的分析是一樣的,就不贅述了。 -
第三次呼叫
iterator.throw(new Error("Boom")
往generator
往拋入錯誤,函式內部在上次停止處即yield first + 2
接收資訊,丟擲錯誤。但是被catch
了,所以繼續執行到下一個停頓點:yield second + 3; // 6 + 3 複製程式碼
最後返回本次迭代結果
{ value: 9, done: false }
-
繼續執行其他迭代,和上沒無甚不同,不贅述。
小結: 這裡有可以看到,
next()
和throw()
都可以讓iterator
繼續執行下去,不同的是後者會是以丟擲錯誤的方式讓iterator
繼續執行的。但在這之後,generator
裡會發生什麼,取決於程式碼怎麼寫的了。
3. Generator
裡的 return
語句
這裡的 return
語句, 功能上與一般函式的 return
沒太大區別,都會阻止 return
之後的語句執行。
function* createIterator() {
yield 1;
return;
yield 2;
yield 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
複製程式碼
上面的 return
, 使得之後的 yield
都被忽略了,所以,迭代二次而卒。
但是,如果 return
後有值,會被計入本次迭代的結果中:
function* createIterator() {
yield 1;
return 42;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 42, done: true }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
複製程式碼
這個iterator
執行兩次就可收攤了,和上一個例子不同的是,最後一次返回結果裡有 return
後的值 { value: 42, done: true }
。
又但是,這個返回值只能用一次,所以第三次執行next
, 返回結果變成了 { value: undefined, done: true }
。
特別注意: 展開操作符...
和 for-of
看到迭代結果裡 done
是 true
就馬上停止執行,連 return
後面的值也不管了,停止得很決絕。如上面的例子,用for-of
和 ...
執行:
function* createIterator() {
yield 1;
return 42;
}
let iterator = createIterator();
for(let item of iterator) {
console.log(item);
}
// 1
let anotherIterator = createIterator();
console.log([...anotherIterator])
// [1]
// 猜猜 [...iterator] 的結果是什麼
複製程式碼
4. Generator 委託
generator
委託是什麼,簡單說就是把 generator
A 委託給 generator
B, 讓 B 代為執行:
function* createNumberIterator() {
yield 1;
yield 2;
}
function* createColorIterator() {
yield "red";
yield "green";
}
function* createCombinedIterator() {
yield* createNumberIterator();
yield* createColorIterator();
yield true;
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "red", done: false }"
console.log(iterator.next()); // "{ value: "green", done: false }"
console.log(iterator.next()); // "{ value: true, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
複製程式碼
以上可見,委託的語法,就是在一個 generator
裡, 用 yield*
操作另一個 generator
的執行結果。
通過委託把不同的 generator
放一起,再利用return
的返回值,可以在 generator
裡通訊,給出了更多的想象空間:
function* createNumberIterator() {
yield 1;
yield 2;
return 3;
}
function* createRepeatingIterator(count) {
for (let i = 0; i < count; i++) {
yield "repeat";
}
}
function* createCombinedIterator() {
let result = yield* createNumberIterator();
yield* createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
複製程式碼
如上, createNumberIterator
的返回值 3
傳入了createRepeatingIterator
裡, 如果拆開寫,是這樣:
function* createNumberIterator() {
yield 1;
yield 2;
return 3;
}
function* createRepeatingIterator(count) {
for (let i = 0; i < count; i++) {
yield "repeat";
}
}
function* createCombinedIterator() {
let result = yield* createNumberIterator();
yield result;
yield* createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
複製程式碼
注意:既然
yield *
後面接的是generator
的執行結果,而generator
是iterable
。就是說,yield *
後可以直接跟iterable
, 如字串。如:
let g = function *() {
yield *['a', 'b', 'c']
}
for(let item of g()) {
console.log(item);
}
// a
// b
// c
複製程式碼
5. Genarator
與非同步
關於 js
裡非同步的特點,這裡展開說了。簡單來講,它讓 js
這們單執行緒語言更強大; 但是,非同步情況一複雜比如有非同步之間有依賴,那就很容易寫出如下的callback hell
, 極難維護:
合理利用 genarator
就可以用同步的寫法,寫非同步。
從之前的介紹裡已經知道,genarator
返回 iterator
, 需要手動呼叫 next
, 很麻煩。那如果封裝一些,可以讓 iterator
自己執行完畢,不就很好了:
-
前期準備,實現自動執行
generator
的函式run(function* () { let value = yield 1; console.log(value); value = yield value + 3; console.log(value); }); 複製程式碼
要讓它自己執行,那麼
run
需要:- 執行
generator
, 拿到iterator
; - 呼叫
iterator.next()
; - 把上一步的返回結果作為下一次
iterator.next(lastResult)
引數,繼續迭代; - 重複 3 ,直到迭代完畢。
實現如下:
function run(taskDef) { // 建立並儲存 iterator,留到後面使用 let task = taskDef(); let result = task.next(); // 遞迴地執行 `next` function step() { // 如果沒完的話 if (!result.done) { result = task.next(result.value); step(); } } // 開始處理 step(); } 複製程式碼
- 執行
-
實現目標,用同步方式寫非同步
加入我們要讓下面這段程式碼可行:
const asyncWork = new Promise((resolve, reject) => { setTimeout(() => resolve(5), 500) }) run(function* () { let value = yield asyncWork; console.log(value) value = yield value + 3; console.log(value) }); 複製程式碼
這裡和上一個例子不同的地方在於,
yield
返回結果可能是個promise
, 那我們加個判斷就可以了:if (result.value && typeof result.value.then === 'function') { result.value.then(d => { result = task.next(d) ... }) } 複製程式碼
就是判斷如果是
promise
, 執行then
函式,把返回結果傳入下一次迭代next(d)
即可。完整示例程式碼如下:function run(taskDef) { // 建立並儲存 iterator,留到後面使用 let task = taskDef(); let result = task.next(); // 遞迴地執行 `next` function step() { // 如果沒完的話 if (!result.done) { if (result.value && typeof result.value.then === 'function') { result.value.then(d => { result = task.next(d) step(); }) } else { result = task.next(result.value); step(); } } } // 開始處理 step(); } 複製程式碼
回頭看看這個寫法:
run(function* () { let value = yield asyncWork; console.log(value) value = yield value + 3; console.log(value) }); 複製程式碼
雖然第二個
yield
對上一個yield
結果有依賴,但不用寫成回撥,看著跟同步一樣,很直白!
結語
generator
產生的 iterator
, 可以用next
,在函式外部往 generator
裡傳資料, 又可以通過 throw
往裡拋錯。它們相當於在 generator
裡對外開啟了多個通訊視窗,這讓清晰的非同步成為可能。強大的 redux-saga
也是基於 generator
實現的。是不是有更多的玩法?一切都是拋磚引玉,不知道大家還有其他玩法沒?
如果對 generator
由來不太清楚的,也可以先看看 這裡
另外,這篇文章最先發布在 github,是個關於 ES6
的系列文章。如果覺得可以,幫忙 star
下唄,方便找工作啊。哎,找工作,真-是-累-啊!!!