跪拜 Guibai
← All articles
Frontend · JavaScript

JavaScript Generators Unpacked: Lazy Pipelines, Two-Way yield, and Async Pagination

By vipxieliang ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Frontend apps that pull large datasets or live streams can't load everything at once. Generator-based pagination and lazy pipelines keep memory bounded and cancel pending work the moment the consumer is done, which directly cuts API calls and improves responsiveness.

Summary

JavaScript's traversal machinery rests on two protocols: the iterable protocol, which requires Symbol.iterator to return an iterator, and the iterator protocol, which requires next() to return { value, done }. Together they let for...of, spreads, and destructuring work uniformly across arrays, strings, Maps, Sets, even NodeList. Hand-writing iterators works, but generator functions declared with function* collapse the state management into a pause-resume flow where each yield hands out a value and next(value) feeds one back in. yield* delegates iteration to another iterable, and return() and throw() give external control for cleanup or errors.

Generators shine when data is large or arrives asynchronously. Lazy evaluation means a generator computes one value per next() call, so infinite sequences like naturals() run without exhausting memory. Composing map, filter, and take generators builds lazy pipelines that only compute what the consumer actually reads. Async generators, declared with async function*, use for await...of and return a Promise from next(), which suits paginated APIs, WebSocket messages, and file streams. A paginate generator can pull page by page, yield individual items, and stop the whole loop when the consumer breaks, avoiding extra requests.

The article also flags the usual pitfalls: generators are single-use and need a fresh call to reset; cleanup belongs in try/finally; side effects inside generators are risky because execution timing is lazy; async errors need try/catch around for await...of; and generator overhead isn't worth it for tiny data sets.

Takeaways
— An object becomes iterable by implementing Symbol.iterator, which must return an iterator object.
— The iterator protocol requires next() to return { value, done }, where done: true signals termination.
— Generator functions are declared with function* and use yield to produce values; calling them returns an iterator without executing the body.
— next(value) sends a value back into the generator, and the first next() argument is ignored because no yield is waiting.
— yield* delegates iteration control to another iterable, letting generators compose.
— return(value) and throw(error) let callers terminate a generator early or inject an error; finally blocks run on cleanup.
— Lazy evaluation lets generators represent infinite sequences and only computes each value when next() is called.
— Chaining map, filter, and take generators builds lazy pipelines that compute no more than the consumer requests.
— Async generators use async function* and for await...of; their next() returns a Promise resolving to { value, done }.
— A paginate async generator can yield items one at a time, stop when the list is empty, and cancel its own fetch loop when the consumer breaks.
— Generators are single-use; consumers must call the generator function again to restart iteration.
— Side effects inside generators are risky because lazy evaluation delays execution to an unpredictable point.
— Wrap for await...of in try/catch to handle errors from async generators.
Conclusions

The two-way yield channel makes generators a coroutine-like control structure, which is a different mental model from promises or callbacks and suits state machines well.

Lazy evaluation changes when code runs, so moving side effects into a generator can produce surprises; keeping generators pure makes them predictable.

The break-based cancellation in async pagination is a clean way to stop network work, because the generator's loop terminates with the consumer, avoiding orphaned requests.

Concepts & terms
Iterable protocol
A JavaScript object satisfies the iterable protocol when it implements Symbol.iterator, a method that returns an iterator. This makes the object usable with for...of, spread, and destructuring.
Iterator protocol
An iterator is an object with a next() method that returns { value, done }. done becomes true when iteration ends, and value holds the current item.
Generator function
A function declared with function* that yields values via yield. Calling it returns an iterator that executes lazily, pausing at each yield until next() is called.
Lazy evaluation
A computation strategy where values are produced only when requested. Generators make this possible by pausing after each yield, so infinite sequences and large datasets can be processed incrementally.
Async generator
A generator declared with async function* that yields asynchronous values. Its iterator's next() returns a Promise, and it is consumed with for await...of.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗