把「資料使用者」與「資料生產者」拆散的設計 - JavaScript Generator 生成器函式
Sat Sep 04 2021
前端開發Generator 生成器是 ES6 新加入的功能,是一種特殊形式的函式,執行後的返回值是一個 Generator 物件,這個物件是可以被迭代的,所以也可以被解構成陣列來使用。
使用 Generator 的主要目的是把「資料使用者」與「資料生產者」之間的關聯解耦,不需要等待資料完整生成後才提供使用,就像去餐廳吃飯你不會希望廚師把所有料理都完成才上桌,這樣不但要餓著肚子等待,上菜時桌子還可能放不下,對吧?
在資料量不大的情況下完整生成後再使用是很常見的做法,但如果資料量龐大或是需要非同步操作的情況,使用 Generator 就會是比較合理的方案,而且就算只是簡單將陣列值逐步取出這樣的操作,透過 Generator 也可以提升可讀性。
# Generator function 與普通 function 的差異
普通函式: 呼叫後只會回傳一個數值,而後結束運行。
生成器函式: 呼叫後會回傳一個迭代器物件,並不是直接開始執行函式。 可以隨著個別的請求來產生一連串的值,且在不同請求之間暫停程式的執行
# 宣告方式與使用方式
最基本的使用方式就像下面這樣(雖然純範例沒有什麼實用性)
function* Generator() {
yield 'Generator'
yield '生成器'
}
const g = Generator()
console.log(g.next())
// {value: "Generator", done: false}
console.log(g.next())
// {value: "生成器", done: false}
console.log(g.next())
// {value: undefined, done: true}
# 多重生成器
不過其實 Generator 還可以接上其他的 Generator 使用。
function* Generator() {
yield 'Generator'
yield* Other()
yield '生成器'
}
function* Other() {
yield 'other'
yield '其他'
}
for(let content of Generator()) {
console.log(content)
}
// Generator
// other
// 其他
// 生成器
陣列或 Map 之類可以迭代的物件也都能使用 yield*
喔!
const map = new Map(Object.entries({ a: 1, b: 2 }))
function* Gen(map) {
yield* map
}
const generator = Gen(map)
generator.next()
// { done: false, value: ['a', 1] }
generator.next()
// { done: false, value: ['b', 2] }
generator.next()
// { done: true, value: undefined }
# 使用 return 明確終止
如果使用 return
而不是 yield
的話,當函示執行到此時,就會直接回傳 done: true
並終止迭代囉,不過放心,這次的 value
還是可以好好取得內容的。
function* yieldAndReturn() {
yield "Y"
return "R" //顯式返回處,可以觀察到 done 也立即變為了 true
yield "unreachable" // 不會被執行了
}
var gen = yieldAndReturn()
gen.next()
// { value: "Y", done: false }
gen.next()
// { value: "R", done: true }
gen.next()
// { value: undefined, done: true }
# 使用範例
# 流水號計數器
這算是一個很簡單卻實用的小小應用情境。
function* IdGenerator() {
let id = 0
while(true) {
yield ++id
}
}
const idIterator = IdGenerator();
爾後只要需要計數的位置,就可以使用:
idIterator.next().value;
# 大神範例
最後就讓我引用一下兔兔教的 Hello 大神的一段 Code 來結束這回合吧,這段 Code 解決了大量運算阻塞了 JavaScript 主要執行緒的問題,可以好好體會這段 Code 的奧妙之處:
function* Gen(list) {
yield* list;
}
const nextTick = () => new Promise((resolve) => setTimeout(resolve));
async function loop(results, gen) {
const { value, done } = gen.next();
if (done) return results;
await nextTick();
if (value % 2 === 0) results.push(value);
return loop(results, gen);
}
async function main() {
const data = Array.from({ length: 1000000 }, (v, i) => i);
const results = await loop([], Gen(data));
console.log(results);
}
main();