Generator
Generator Definition
- 函数名称前面加一个星号 (
*) 表示它是一个生成器函数. - 箭头函数不能用来定义生成器函数.
- 调用生成器函数会产生一个生成器对象, 其是一个自引用可迭代对象:
其本身是一个迭代器, 同时实现了
Iterable接口 (返回this).
interface GeneratorFunction {
(...args: any[]): Generator
readonly length: number
readonly name: string
readonly prototype: Generator
}
interface Generator<T> extends Iterator<T> {
next: (...args: []) => IteratorResult<T>
return: (value: T) => IteratorResult<T> // Required
throw: (e: any) => IteratorResult<T> // Required
[Symbol.iterator]: () => Generator<T>
}
interface AsyncGeneratorFunction {
(...args: any[]): AsyncGenerator
readonly length: number
readonly name: string
readonly prototype: AsyncGenerator
}
interface AsyncGenerator<T> extends AsyncIterator<T> {
next: (...args: []) => Promise<IteratorResult<T>>
return: (value: T | PromiseLike<T>) => Promise<IteratorResult<T>> // Required
throw: (e: any) => Promise<IteratorResult<T>> // Required
[Symbol.asyncIterator]: () => AsyncGenerator<T>
}
function* generatorFn() {}
console.log(generatorFn)
// f* generatorFn() {}
console.log(generatorFn()[Symbol.iterator])
// f [Symbol.iterator]() {native code}
console.log(generatorFn())
// generatorFn {<suspended>}
console.log(generatorFn()[Symbol.iterator]())
// generatorFn {<suspended>}
const g = generatorFn() // IterableIterator
console.log(g === g[Symbol.iterator]())
// true
Generator Roles
Generators can play 3 roles:
- Iterators (data producers): generators can produce sequences of values via loops and recursion.
- Observers (data consumers):
generators become data consumers that pause
until a new value is pushed into them via
next(value)(yieldcan receive a value fromnext(value)). - Coroutines (data producers and consumers): generators are pauseable and can be both data producers and data consumers, generators can be coroutines (cooperatively multi-tasked tasks).
Generators can help reduce tight coupling:
let windowStart = 0
function calculateMovingAverage(values, windowSize) {
const section = values.slice(windowStart, windowStart + windowSize)
if (section.length < windowSize)
return null
return section.reduce((sum, val) => sum + val, 0) / windowSize
}
loadButton.addEventListener('click', () => {
const avg = calculateMovingAverage(prices, 5)
average.innerHTML = `Average: $${avg}`
windowStart++
})
windowStart is only exposed where needed,
state and logic are self-contained with generators:
function* calculateMovingAverage(values, windowSize) {
let windowStart = 0
while (windowStart <= values.length - 1) {
const section = values.slice(windowStart, windowStart + windowSize)
yield section.reduce((sum, val) => sum + val, 0) / windowSize
windowStart++
}
}
const generator = calculateMovingAverage(prices, 5)
loadButton.addEventListener('click', () => {
const { value } = generator.next()
average.innerHTML = `Average: $${value}`
})
Ugly callback-based code:
async function monitorVitals(cb) {
while (true) {
await new Promise(r => setTimeout(r, 1000))
const vitals = await requestVitals()
cb(vitals)
}
}
monitorVitals((vitals) => {
console.log('Update the UI...', vitals)
})
No callbacks and timing risks with generators:
async function* generateVitals() {
while (true) {
const result = await requestVitals()
await new Promise(r => setTimeout(r, 1000))
yield result
}
}
for await (const vitals of generateVitals()) {
console.log('Update the UI...', vitals)
}
Generator Basic Usage
function* gen() {
yield 1
yield 2
yield 3
}
const g = gen()
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
g.return() // { value: undefined, done: true }
g.return(1) // { value: 1, done: true }
Default Iterator Generator
生成器函数和默认迭代器被调用之后都产生迭代器 (生成器对象是自引用可迭代对象, 自身是一个迭代器), 所以生成器适合作为默认迭代器:
const users = {
james: false,
andrew: true,
alexander: false,
daisy: false,
luke: false,
clare: true,
* [Symbol.iterator]() {
// this === 'users'
for (const key in this) {
if (this[key])
yield key
}
},
}
for (const key of users)
console.log(key)
// andrew
// clare
class Foo {
constructor() {
this.values = [1, 2, 3]
}
* [Symbol.iterator]() {
yield* this.values
}
}
const f = new Foo()
for (const x of f)
console.log(x)
// 1
// 2
// 3
Early Return Generator
return()方法会强制生成器进入关闭状态.- 提供给
return()的值, 就是终止迭代器对象的值.
function* gen() {
yield 1
yield 2
yield 3
}
const g = gen()
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
Error Handling Generator
throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中. 如果错误未被处理, 生成器就会关闭.- 假如生成器函数内部处理了这个错误, 那么生成器就不会关闭, 可以恢复执行. 错误处理会跳过对应的 yield (跳过一个值).
function* generator() {
try {
yield 1
} catch (e) {
console.log(e)
}
yield 2
yield 3
yield 4
yield 5
}
const it = generator()
it.next() // {value: 1, done: false}
// the error will be handled and printed ("Error: Handled!"),
// then the flow will continue, so we will get the
// next yielded value as result.
it.throw(new Error('Handled!')) // {value: 2, done: false}
it.next() // {value: 3, done: false}
// now the generator instance is paused on the
// third yield that is not inside a try-catch.
// the error will be re-thrown out
it.throw(new Error('Not handled!')) // !!! Uncaught Error: Not handled! !!!
// now the iterator is exhausted
it.next() // {value: undefined, done: true}
Generator Advanced Usage
Next Value Generator
当为 next 传递值进行调用时,
传入的值会被当作上一次生成器函数暂停时 yield 关键字的返回值处理.
第一次调用 g.next() 传入参数是毫无意义,
因为首次调用 next 函数时,
生成器函数并没有在 yield 关键字处暂停:
function* lazyCalculator(operator) {
const firstOperand = yield
const secondOperand = yield
switch (operator) {
case '+':
yield firstOperand + secondOperand
return
case '-':
yield firstOperand - secondOperand
return
case '*':
yield firstOperand * secondOperand
return
case '/':
yield firstOperand / secondOperand
return
default:
throw new Error('Unsupported operation!')
}
}
const g = gen('*')
g.next() // { value: undefined, done: false }
g.next(10) // { value: undefined, done: false }
g.next(2) // { value: 20, done: false }
g.next() // { value: undefined, done: true }
Default Asynchronous Iterator Generator
Default asynchronous iterator:
const asyncSource = {
async* [Symbol.asyncIterator]() {
yield await new Promise(resolve => setTimeout(resolve, 1000, 1))
},
}
for await (const chunk of asyncSource)
console.log(chunk)
Asynchronous Generator
async function* remotePostsAsyncGenerator() {
let i = 1
while (true) {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${i++}`
).then(r => r.json())
// when no more remote posts will be available,
// it will break the infinite loop.
// the async iteration will end
if (Object.keys(res).length === 0)
break
yield res
}
}
for await (const chunk of remotePostsAsyncGenerator())
console.log(chunk)
Asynchronous Events Stream
Asynchronous UI events stream (RxJS):
class Observable {
constructor() {
this.promiseQueue = []
// 保存用于队列下一个 promise 的 resolve 方法
this.resolve = null
// 把最初的 promise 推到队列, 该 promise 会 resolve 为第一个观察到的事件
this.enqueue()
}
// 创建新 promise, 保存其 resolve 方法, 并把它保存到队列中
enqueue() {
this.promiseQueue.push(new Promise(resolve => (this.resolve = resolve)))
}
// 从队列前端移除 promise, 并返回它
dequeue() {
return this.promiseQueue.shift()
}
async* fromEvent(element, eventType) {
// 在有事件生成时, 用事件对象来 resolve 队列头部的 promise
// 同时把另一个 promise 加入队列
element.addEventListener(eventType, (event) => {
this.resolve(event)
this.enqueue()
})
// 每次 resolve 队列头部的 promise 后, 都会向异步迭代器返回相应的事件对象
while (true)
yield await this.dequeue()
}
}
const observable = new Observable()
const button = document.querySelector('button')
const mouseClickIterator = observable.fromEvent(button, 'click')
for await (const clickEvent of mouseClickIterator)
console.log(clickEvent)
Generator based asynchronous control flow goodness for nodejs and the browser, using promises, letting you write non-blocking code in a nice-ish way (just like tj/co).
function coroutine(generatorFunc) {
const generator = generatorFunc()
function nextResponse(value) {
const response = generator.next(value)
if (response.done)
return
if (value instanceof Promise)
value.then(nextResponse)
else
nextResponse(response.value)
}
nextResponse()
}
coroutine(function* bounce() {
yield bounceUp
yield bounceDown
})
利用 async/await 可以实现相同效果:
function co(gen) {
return new Promise((resolve, reject) => {
const g = gen()
function next(param) {
const { done, value } = g.next(param)
if (!done) {
// Resolve chain.
Promise.resolve(value).then(res => next(res))
} else {
resolve(value)
}
}
// First invoke g.next() without params.
next()
})
}
function promise1() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('1')
}, 1000)
})
}
function promise2(value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`value:${value}`)
}, 1000)
})
}
function* readFileGenerator() {
const value = yield promise1()
const result = yield promise2(value)
return result
}
async function readFile() {
const value = await promise1()
const result = await promise2(value)
return result
}
co(readFileGenerator).then(res => console.log(res))
// const g = readFileGenerator();
// const value = g.next();
// const result = g.next(value);
// resolve(result);
readFile().then(res => console.log(res))
Delegating Generator
yield* 能够迭代一个可迭代对象 (yield* iterable):
- 可以迭代标准库提供的
Iterable集合. - 生成器函数产生的生成器对象是一个自引用可迭代对象,
可以使用
yield*聚合生成器 (Delegating Generator).
function* generatorFn() {
console.log('iter value:', yield* [1, 2, 3])
}
for (const x of generatorFn())
console.log('value:', x)
// value: 1
// value: 2
// value: 3
// iter value: undefined
function* innerGeneratorFn() {
yield 'foo'
return 'bar'
}
function* outerGeneratorFn(genObj) {
console.log('iter value:', yield* innerGeneratorFn())
}
for (const x of outerGeneratorFn())
console.log('value:', x)
// value: foo
// iter value: bar
function* chunkify(array, n) {
yield array.slice(0, n)
array.length > n && (yield* chunkify(array.slice(n), n))
}
async function* getRemoteData() {
let hasMore = true
let page
while (hasMore) {
const { next_page, results } = await fetch(URL, { params: { page } }).then(
r => r.json()
)
// Return 5 elements with each iteration.
yield* chunkify(results, 5)
hasMore = next_page !== null
page = next_page
}
}
for await (const chunk of getRemoteData())
console.log(chunk)
Recursive Generator
在生成器函数内部,
用 yield* 去迭代自身产生的生成器对象,
实现递归算法.
Tree traversal:
// Tree traversal
class BinaryTree {
constructor(value, left = null, right = null) {
this.value = value
this.left = left
this.right = right
}
* [Symbol.iterator]() {
yield this.value
if (this.left) {
// Short for: yield* this.left[Symbol.iterator]()
yield* this.left
}
if (this.right) {
// Short for: yield* this.right[Symbol.iterator]()
yield* this.right
}
}
}
const tree = new BinaryTree(
'a',
new BinaryTree('b', new BinaryTree('c'), new BinaryTree('d')),
new BinaryTree('e')
)
for (const x of tree)
console.log(x)
// Output:
// a
// b
// c
// d
// e
Graph traversal:
// Graph traversal
function* graphTraversal(nodes) {
for (const node of nodes) {
if (!visitedNodes.has(node)) {
yield node
yield* graphTraversal(node.neighbors)
}
}
}
DOM traversal:
function* domTraversal(element) {
yield element
element = element.firstElementChild
while (element) {
yield* domTraversal(element)
element = element.nextElementSibling
}
}
for (const element of domTraversal(document.getElementById('subTree')))
console.log(element.nodeName)
结合 Promise/async/await 可以实现异步递归算法:
import { promises as fs } from 'node:fs'
import { basename, dirname, join } from 'node:path'
async function* walk(dir: string): AsyncGenerator<string> {
for await (const d of await fs.opendir(dir)) {
const entry = join(dir, d.name)
if (d.isDirectory())
yield* walk(entry)
else if (d.isFile())
yield entry
}
}
async function run(arg = '.') {
if ((await fs.lstat(arg)).isFile())
return runTestFile(arg)
for await (const file of walk(arg)) {
if (
!dirname(file).includes('node_modules')
&& (basename(file) === 'test.js' || file.endsWith('.test.js'))
) {
console.log(file)
await runTestFile(file)
}
}
}