把「資料使用者」與「資料生產者」拆散的設計 - 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();