這篇要來說明 Generator 產生器,不過在那之前要先來了解另一個也跟它有關的東西-Iterator 疊代器。
在 JS 中,有些型別是可以迭代的像是 Array/TypeArray、Set、Map、String、函數中的 arguments、NodeList。
而這些型別當中,從原型鏈往上查找,會找到 @@iterator
屬性,像陣列就有 Array.prototype[@@iterator]()
,透過呼叫 @@iterator
就可以迭代陣列。
Symbol.iterator
可讓物件實現可迭代的特性。在此範例中,Symbol.iterator
會回傳 Iterator 物件,這個 Iterator 就是一個拜訪資料結構的 pointer,每次呼叫 next() 方法,就會回傳物件,裡面包括當前步驟返回的值 value 和是否完成的狀態 done。
const nums = [1, 2, 3];
const numsIterator = nums[Symbol.iterator]();
console.log(numsIterator.next()); // {value: 1, done: false}
console.log(numsIterator.next()); // {value: 2, done: false}
console.log(numsIterator.next()); // {value: 3, done: false}
console.log(numsIterator.next()); // {value: undefined, done: true}
如果要客製化迭代器,可以參考以下範例:
const customIterable = {
data: [1, 2, 3, 4],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.data.length) {
return { value: this.data[index++], done: false };
} else {
return { value: undefined, done: true };
}
},
};
},
};
// 使用 for...of 遍歷
for (const value of customIterable) {
console.log(value);
}
// 使用迭代器手動遍歷
const iterator = customIterable[Symbol.iterator]();
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: 4, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
有了 Iterator 的概念後,接著來看 Generator,它和一般的函式不同,一般的函式呼叫後就會持續執行直到結束,但 Generator 可以使用 yield 和 next() 暫停和繼續執行,幫使用者維護其內部的狀態。
以下來看幾個範例:
在此範例中可以看到 names 函式加上了 *,這是用來告訴 JS 這是一個 Generator 函式,而函式內部使用了 yield 關鍵字來定義內部執行狀態。
在運作過程中,函式會以起始狀態 => 繼續(執行 next()
方法) => 暫停(遇到 yield 關鍵字) => 繼續 => 暫停...不斷反覆到結束(沒有輸出值時)。並且每次執行函式時會返回一個 Generator 物件(但本質是 Iterator 物件),如下方範例中看到的 console 值。
function* names() {
yield 'Tom';
yield 'Harry';
yield 'Jerry';
}
const getName = names();
console.log(getName.next()); // {value: "Tom", done: false}
console.log(getName.next()); // {value: "Harry", done: false}
console.log(getName.next()); // {value: "Jerry", done: false}
console.log(getName.next()); // {value: undefined, done: true}
infinite loop,只要不 return 該函式,它就可以無止盡呼叫 next() 執行下去。
function* generateCount() {
let count = 0;
while (true) {
const increment = yield count; // increment 為 next() 傳入的參數,透過 yield 返回
increment != null ? count += increment : count++;
}
}
const generatorObj = generateCount();
console.log(generatorObj.next()); // { done: false, value: 0 },這邊傳入參數無效,yield 還不會回傳
console.log(generatorObj.next()); // { done: false, value: 1 }
console.log(generatorObj.next(3)); // { done: false, value: 4 }
console.log(generatorObj.next()); // { done: false, value: 5 }
console.log(generatorObj.return()); // { done: false, value: undefined },離開 generator 函式
console.log(generatorObj.throw(new Error('Error!!'))); // 拋出錯誤
另外,除了 next() 外,Generator 還有 return() & throw() 可以結束函式和拋出錯誤。
在了解 Generator 後,前面提到的都是 Generator 的一些範例程式,有沒有實際在專案或是函式庫、套件等地方使用的案例呢?
答案是有,上篇提到的 async/await 就是一種語法糖,背後的運作原理就是 Generator。
以下是來自 High-performance ES2015 and beyond 的一段話:
Async functions are essentially sugar on top of generators, so they fall into the same category.
除了這個例子之外,有名的 Redux middleware - redux-saga 也使用到了 Generator。
以下範例節錄自 redux-saga 官網教學:
import { put, takeEvery } from 'redux-saga/effects'
const delay = (ms) => new Promise(res => setTimeout(res, ms))
// ...
// Our worker Saga: will perform the async increment task
export function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}
// Our watcher Saga: spawn a new incrementAsync task on each INCREMENT_ASYNC
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
Learn JavaScript Generators In 12 Minutes
ES6 Generators in JavaScript, a Real-World Use Case