我曾一度認爲沒有必要去學習 Javascript 的生成器( Generator ),認爲它只是解決異步行爲的一種過渡解決方案,直到最近對相關工具庫的深刻學習,才逐漸認識到其強大之處。可能你並無手動去寫過一個生成器,可是不得不否定它已經被普遍使用,尤爲是在 redux-saga 和 RxJS 等優秀的開源工具。javascript
首先須要明確的是生成器其實來源於一種設計模式 —— 迭代器模式,在 Javascript 中迭代器模式的表現形式是可迭代協議,而這也是 ES2015 中迭代器和可迭代對象的來源,這是兩個容易讓人混淆的概念,但事實上 ES2015 對其作了明確區分。java
實現了 next
方法的對象被稱爲迭代器。next
方法必須返回一個 IteratorResult
對象,該對象形如:node
{ value: undefined, done: true }
複製代碼
其中 value
表示這次迭代的結果,done
表示是否已經迭代完畢。react
實現了 @@iterator
方法的對象稱爲可迭代對象,也就是說該對象必須有一個名字是 [Symbol.iterator]
的屬性,這個屬性是一個函數,返回值必須是一個迭代器。git
String
, Array
, TypedArray
, Map
和 Set
是 Javascript 中內置的可迭代對象,好比,Array.prototype[Symbol.iterator]
和 Array.prototype.entries
會返回同一個迭代器:es6
const a = [1, 3, 5];
a[Symbol.iterator]() === a.entries(); // true
const iter = a[Symbol.iterator](); // Array Iterator {}
iter.next() // { value: 1, done: false }
複製代碼
ES2015 中新增的數組解構也會默認使用迭代器進行迭代:github
const arr = [1, 3, 5];
[...a]; // [1, 3, 5]
const str = 'hello';
[...str]; // ['h', 'e', 'l', 'l', 'o']
複製代碼
既然可迭代對象是實現了 @@iterator
方法的對象,那麼可迭代對象就能夠經過重寫 @@iterator
方法實現自定義迭代行爲:編程
const arr = [1, 3, 5, 7];
arr[Symbol.iterator] = function () {
const ctx = this;
const { length } = ctx;
let index = 0;
return {
next: () => {
if (index < length) {
return { value: ctx[index++] * 2, done: false };
} else {
return { done: true };
}
}
};
};
[...arr]; // [2, 6, 10, 14]
複製代碼
從上面能夠看出,當 next
方法返回 { done: true }
時,迭代結束。json
有兩種方法返回生成器:redux
function*
聲明的函數是一個生成器函數,生成器函數返回的是一個生成器const counter = (function* () {
let c = 0;
while(true) yield ++c;
})();
counter.next(); // { value: 1, done: false },counter 是一個迭代器
counter[Symbol.iteratro]();
// counterGen {[[GeneratorStatus]]: "suspended"}, counter 是一個可迭代對象
複製代碼
上面的代碼中的 counter
就是一個生成器,實現了一個簡單的計數功能。不只沒有使用閉包也沒有使用全局變量,實現過程很是優雅。
生成器的強大之處在於能方便地對生成器函數內部的邏輯進行控制。在生成器函數內部,經過 yield
或 yield*
,將當前生成器函數的控制權移交給外部,外部經過調用生成器的 next
或 throw
或 return
方法將控制權返還給生成器函數,而且還可以向其傳遞數據。
yield
和 yield*
只能在生成器函數中使用。生成器函數內部經過 yield
提早返回,前面的計數器就是利用這個特性向外部傳遞計數的結果。須要注意的是前面的計數器是無限執行的,只要生成器調用 next
方法,IteratorResult
的 value
就會一直遞增下去,若是想計數個有限值,須要在生成器函數裏面使用 return
表達式:
const ceiledCounter = (function* (ceil) {
let c = 0;
while(true) {
++c;
if (c === ceil) return c;
yield c;
}
})(3);
ceiledCounter.next(); // { value: 1, done: false }
ceiledCounter.next(); // { value: 2, done: false }
ceiledCounter.next(); // { value: 3, done: true }
ceiledCounter.next(); // { value: undefined, done: true }
複製代碼
yield
後能夠不帶任何表達式,返回的 value
爲 undefined
:
const gen = (function* () {
yield;
})();
gen.next(); // { value: undefined, done: false }
複製代碼
生成器函數經過使用 yield*
表達式用於委託給另外一個可迭代對象,包括生成器。
委託給 Javascript 內置的可迭代對象:
const genSomeArr = function* () {
yield 1;
yield* [2, 3];
};
const someArr = genSomeArr();
greet.next(); // { value: 1, done: false }
greet.next(); // { value: 2, done: false }
greet.next(); // { value: 3, done: false }
greet.next(); // { value: undefined, done: true }
複製代碼
委託給另外一個生成器(仍是利用上面的 genGreet
生成器函數):
const genAnotherArr = function* () {
yield* genSomeArr();
yield* [4, 5];
};
const anotherArr = genAnotherArr();
greetWorld.next(); // { value: 1, done: false}
greetWorld.next(); // { value: 2, done: false}
greetWorld.next(); // { value: 3, done: false}
greetWorld.next(); // { value: 4, done: false}
greetWorld.next(); // { value: 5, done: false}
greetWorld.next(); // { value: undefined, done: true}
複製代碼
yield
表達式是有返回值的,接下來解釋具體行爲。
生成器函數外部正是經過這三個方法去控制生成器函數的內部執行過程的。
生成器函數外部能夠向 next
方法傳遞一個參數,這個參數會被看成上一個 yield
表達式的返回值,若是不傳遞任何參數,yield
表達式返回 undefined
:
const canBeStoppedCounter = (function* () {
let c = 0;
let shouldBreak = false;
while (true) {
shouldBreak = yield ++c;
console.log(shouldBreak);
if (shouldBreak) return;
}
};
canBeStoppedCounter.next();
// { value: 1, done: false }
canBeStoppedCounter.next();
// undefined,第一次執行 yield 表達式的返回值
// { value: 2, done: false }
canBeStoppedCounter.next(true);
// true,第二次執行 yield 表達式的返回值
// { value: undefined, done: true }
複製代碼
再來看一個連續傳入值的例子:
const greet = (function* () {
console.log(yield);
console.log(yield);
console.log(yield);
return;
})();
greet.next(); // 執行第一個 yield表達式
greet.next('How'); // 第一個 yield 表達式的返回值是 "How",輸出 "How"
greet.next('are'); // 第二個 yield 表達式的返回值是 "are",輸出"are"
greet.next('you?'); // 第三個 yield 表達式的返回值是 "you?",輸出 "you"
greet.next(); // { value: undefined, done: true }
複製代碼
生成器函數外部能夠向 throw
方法傳遞一個參數,這個參數會被 catch
語句捕獲,若是不傳遞任何參數,catch
語句捕獲到的將會是 undefined
,catch
語句捕獲到以後會恢復生成器的執行,返回帶有 IteratorResult
:
const caughtInsideCounter = (function* () {
let c = 0;
while (true) {
try {
yield ++c;
} catch (e) {
console.log(e);
}
}
})();
caughtInsideCounter.next(); // { value: 1, done: false}
caughtIndedeCounter.throw(new Error('An error occurred!'));
// 輸出 An error occurred!
// { value: 2, done: false }
複製代碼
須要注意的是若是生成器函數內部沒有 catch
到,則會在外部 catch
到,若是外部也沒有 catch
到,則會像全部未捕獲的錯誤同樣致使程序終止執行:
生成器的 return
方法會結束生成器,而且會返回一個 IteratorResult
,其中 done
是 true
,value
是向 return
方法傳遞的參數,若是不傳遞任何參數,value
將會是 undefined
:
const g = (function* () {
yield 1;
yield 2;
yield 3;
})();
g.next(); // { value: 1, done: false }
g.return("foo"); // { value: "foo", done: true }
g.next(); // { value: undefined, done: true }
複製代碼
經過上面三個方法,使得生成器函數外部對生成器函數內部程序執行流程有了一個很是強的控制力。
生成器函數與異步操做結合是很是天然的表達:
const fetchUrl = (function* (url) {
const result = yield fetch(url);
console.log(result);
})('https://api.github.com/users/github');
const fetchPromise = fetchUrl.next().value;
fetchPromise
.then(response => response.json())
.then(jsonData => fetchUrl.next(jsonData));
// {login: "github", id: 9919, avatar_url: "https://avatars1.githubusercontent.com/u/9919?v=4", gravatar_id: "", url: "https://api.github.com/users/github", …}
複製代碼
在上面的代碼中,fetch
方法返回一個 Promise
對象 fetchPromise
,fetchPromise
經過一系列的解析以後會返回一個 JSON 格式的對象 jsonData
,將其經過 fetchUrl
的 next
方法傳遞給生成器函數中的 result
,而後打印出來。
從上面的過程能夠看出,生成器配合 Promise
確實能夠很簡潔的進行異步操做,可是還不夠,由於整個異步流程都是咱們手動編寫的。當異步行爲變的更加複雜起來以後(好比一個異步操做的隊列),生成器的異步流程管理過程也將會變得難以編寫和維護。
須要一種能自動執行異步任務的工具進行配合,生成器才能真正派上用場。實現這種工具一般有兩種思路:
Promise
對象,將異步過程扁平化處理,基於這種思路的是 co 模塊;下面來分別理解和實現。
thunk
函數的起源其實很早,而 thunkify
模塊也做爲異步操做的一種廣泛解決方案,thunkify
的源碼很是簡潔,加上註釋也才三十行左右,建議全部學習異步編程的開發者都去閱讀一遍。
理解了 thunkify
的思想以後,能夠將其刪減爲一個簡化版本(只用於理解,不用於生產環境中):
const thunkify = fn => {
return (...args) => {
return callback => {
return Reflect.apply(fn, this, [...args, callback]);
};
};
};
複製代碼
從上面的代碼能夠看出,thunkify
函數適用於回調函數是最後一個參數的異步函數,下面咱們構造一個符合該風格的異步函數便於咱們調試:
const asyncFoo = (id, callback) => {
console.log(`Waiting for ${id}...`)
return setTimeout(callback, 2000, `Hi, ${id}`)
};
複製代碼
首先是基本使用:
const foo = thunkify(asyncFoo);
foo('Juston')(greetings => console.log(greetings));
// Waiting for Juston...
// ... 2s later ...
// Hi, Juston
複製代碼
接下來咱們模擬實際需求,實現每隔 2s 輸出一次結果。首先是構造生成器函數:
const genFunc = function* (callback) {
callback(yield foo('Carolanne'));
callback(yield foo('Madonna'));
callback(yield foo('Michale'));
};
複製代碼
接下來實現一個自動執行生成器的輔助函數 runGenFunc
:
const runGenFunc = (genFunc, callback, ...args) => {
const g = genFunc(callback, ...args);
const seqRun = (data) => {
const result = g.next(data);
if (result.done) return;
result.value(data => seqRun(data));
}
seqRun();
};
複製代碼
注意 g.next().value
是一個函數,而且接受一個回調函數做爲參數,runGenFunc
經過第 7 行的代碼實現了兩個關鍵步驟:
yield
表達式的結果返回之生成器函數yield
表達式最後是調用 runGenFunc
而且將 genFunc
、須要用到的回調函數 callback
以及其餘的生成器函數參數(這裏的生成器函數只有一個回調函數做爲參數)傳入:
runGenFunc(genFunc, greetings => console.log(greetings));
// Waiting for Carolanne...
// ... 2s later ...
// Hi, Carolanne
// Waiting for Madonna...
// ... 2s later ...
// Hi, Madonna
// Waiting for Michale...
// ... 2s later ...
// Hi, Michale
複製代碼
能夠看到輸出結果確實如指望的那樣,每隔 2s 進行一次輸出。
從上面的過程來看,使用 thunkify
模塊進行異步流程的管理仍是不夠方便,緣由在於咱們不得不本身引入一個輔助的 runGenFunc
函數來進行異步流程的自動執行。
co 模塊能夠幫咱們完成異步流程的自動執行工做。co
模塊是基於 Promise
對象的。co
模塊的源碼一樣很是簡潔,也比較適合閱讀。
co
模塊的 API 只有兩個:
co(fn*).then(val => )
co
方法接受一個生成器函數爲惟一參數,而且返回一個 Promise
對象,基本使用方法以下:
const promise = co(function* () {
return yield Promise.resolve('Hello, co!');
})
promise
.then(val => console.log(val)) // Hello, co!
.catch((err) => console.error(err.stack));
複製代碼
fn = co.wrap(fn*)
co.wrap
方法在 co
方法的基礎上進行了進一步的包裝,返回一個相似於 createPromise
的函數,它與 co
方法的區別就在與能夠向內部的生成器函數傳遞參數,基本使用方法以下。
const createPromise = co.wrap(function* (val) {
return yield Promise.resolve(val);
});
createPromise('Hello, jkest!')
.then(val => console.log(val)) // Hello, jkest!
.catch((err) => console.error(err.stack));
複製代碼
co
模塊須要咱們將 yield
關鍵字後面的對象改造爲一個 co
模塊自定義的 yieldable 對象,一般能夠認爲是 Promise
對象或基於 Promise
對象的數據結構。
瞭解了 co
模塊的使用方法後,不難寫出基於 co
模塊的自動執行流程。
只須要改造 asyncFoo
函數讓其返回一個 yieldable
對象,在這裏便是 Promise
對象:
const asyncFoo = (id) => {
return new Promise((resolve, reject) => {
console.log(`Waiting for ${id}...`);
if(!setTimeout(resolve, 2000, `Hi, ${id}`)) {
reject(new Error(id));
}
});
};
複製代碼
而後就可使用 co
模塊進行調用,因爲須要向 genFunc
函數傳入一個 callback
參數,因此必須使用 co.wrap
方法:
co.wrap(genFunc)(greetings => console.log(greetings));
複製代碼
上述結果與指望一致。
其實 co
模塊內部的實現方式與 thunkify 小節中的 runGenFunc
函數有殊途同歸之處,都是使用遞歸函數反覆去執行 yield
語句,知道生成器函數迭代結束,主要的區別就在於 co
模塊是基於 Promise
實現的。
可能在實際工做中的大部分時候均可以使用外部模塊去完成相應的功能,可是想理解實現原理或者不想引用外部模塊,則深刻理解生成器的使用就很重要了。在下一篇文章[觀察者模式在 Javascript 中的應用]中我會探究 RxJS 的實現原理,其中一樣涉及到本文所所說起的迭代器模式。最後附上相關參考資料,以供感興趣的讀者繼續學習。