Recently, in order to better understand the working principle of Redux Sagas, I re-learned the knowledge of JavaScript generators. I collected some information from the Internet. Various knowledge points have been condensed into one article. I hope that this article is easy to understand, yet rigorous enough, and can be used as a guide for beginners to use generators.
Introduction
JavaScript introduced generators in ES6. Generator functions are like regular functions, except that they can be paused and resumed. Generators are also closely related to iterators, since generator objects are iterators.
In JavaScript, you generally cannot pause or stop after a function call. (Yes, async functions pause while awaiting an await statement, but async functions were only introduced in ES7. Also, async functions are built on top of generators.) A normal function only ends when it returns or throws an error .
function foo() { console.log('Starting'); const x = 42; console.log(x); console.log('Stop me if you can'); console.log('But you cannot'); }
In contrast, generators allow us to pause execution at any breakpoint and resume execution from the same breakpoint.
Generators and Iterators
From MDN:
In JavaScript, an iterator is an object that defines a sequence and may return a return value when terminated . >More specifically, an iterator is any object that implements the Iterator protocol>by using the next() method, which returns an object with two properties: value, which is the next value in the sequence; and done, if It is true if the last value in the sequence has been iterated to. If value and done are present together, it is the return value of the iterator.
Therefore, the essence of an iterator is:
- The object that defines the sequence
- has a
next()
method... - Return an object with two properties: value and done
Is a generator needed to create an iterator? No. In fact, we can already create an infinite Fibonacci sequence using closures pre-ES6, as shown in the following example:
var fibonacci = { next: (function () { var pre = 0, cur = 1; return function () { tmp = pre; pre = cur; cur += tmp; return cur; }; })() }; fibonacci.next(); // 1 fibonacci.next(); // 2 fibonacci.next(); // 3 fibonacci.next(); // 5 fibonacci.next(); // 8
Regarding the benefits of generators, I will quote MDN again:
Although custom iterators are a useful tool, creating them requires careful programming due to the need to explicitly maintain their internal state. Generator functions provide a powerful alternative: they allow us to define iterative algorithms by writing a function whose execution is not continuous.
In other words, it is simpler to create iterators using generators (no closures required!), which means there is less chance of errors.
The relationship between generators and iterators is that the generator object returned by the generator function is an iterator.
Syntax
Generator functions are created using function* syntax and paused using the yield keyword.
Initially calling a generator function does not execute any of its code; instead, it returns a generator object. This value is used by calling the generator's next() method, which executes the code until the yield keyword is encountered, then pauses until next() is called again.
function * makeGen() { yield 'Hello'; yield 'World'; } const g = makeGen(); // g is a generator g.next(); // { value: 'Hello', done: false } g.next(); // { value: 'World', done: false } g.next(); // { value: undefined, done: true }
Repeated calls to g.next() after the last statement above will only return (or, more accurately, produce) the same return object: { value: undefined, done: true }.
yield pauses execution
You may notice something special about the above code snippet. The second next() call produces an object with the property done: false instead of done: true.
Since we are executing the last statement in the generator function, shouldn't the done attribute be true? not really. When a yield statement is encountered, the value following it (in this case "World") is generated and execution is paused. Therefore, the second next() call pauses on the second yield statement, so execution is not complete yet - execution is not complete until execution resumes after the second yield statement (i.e. done: true), and not Run the code again.
We can think of the next() call as telling the program to run to the next yield statement (assuming it exists), generate a value, and pause. The program will not know that there is nothing after the yield statement until it resumes execution, and execution can only be resumed with another next() call.
yield and return
In the above example, we use yield to pass the value outside the generator. We can also use return (just like in a normal function); however, using return terminates execution and sets done: true.
function * makeGen() { yield 'Hello'; return 'Bye'; yield 'World'; } const g = makeGen(); // g is a generator g.next(); // { value: 'Hello', done: false } g.next(); // { value: 'Bye', done: true } g.next(); // { value: undefined, done: true }
Because execution does not pause on the return statement, and by definition no code can be executed after the return statement, done is set to true.
yield: Parameters for next method
So far we have been using yield to pass values outside the generator (and pause its execution).
However, yield is actually bidirectional and allows us to pass values into generator functions.
function * makeGen() { const foo = yield 'Hello world'; console.log(foo); } const g = makeGen(); g.next(1); // { value: 'Hello world', done: false } g.next(2); // logs 2, yields { value: undefined, done: true }
等一下。不应该是"1"打印到控制台,但是控制台打印的是"2"?起初,我发现这部分在概念上与直觉相反,因为我预期的赋值foo = 1。毕竟,我们将“1”传递到next()方法调用中,从而生成Hello world,对吗?
但事实并非如此。传递给第一个next(...)调用的值将被丢弃。除了这似乎是ES6规范之外,实际上没有其他原因.从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
我喜欢这样对程序的执行进行合理化:
- 在第一个next()调用时,它将一直运行,直到遇到yield 'Hello world',在此基础上生成{ value: 'Hello world', done: false }和暂停。就是这么回事。正如大家所看到的,传递给第一个next()调用的任何值都是不会被使用的(因此被丢弃)。
- 当再次调用next(...)时,执行将恢复。在这种情况下,执行需要为常量foo分配一些值(由yield语句决定)。因此,我们对next(2)的第二次调用赋值foo=2。程序不会在这里停止—它会一直运行,直到遇到下一个yield或return语句。在本例中,没有更多的yield,因此它记录2并返回undefined的done: true。在生成器使用异步因为yield是一个双向通道,允许信息在两个方向上流动,所以它允许我们以非常酷的方式使用生成器。到目前为止,我们主要使用yield在生成器之外传递值。但是我们也可以利用yield的双向特性以同步方式编写异步函数。
使用上面的概念,我们可以创建一个类似于同步代码但实际上执行异步函数的基本函数:
function request(url) { fetch(url).then(res => { it.next(res); // Resume iterator execution }); } function * main() { const rawResponse = yield request('https://some-url.com'); const returnValue = synchronouslyProcess(rawResponse); console.log(returnValue); } const it = main(); it.next(); // Remember, the first next() call doesn't accept input
这是它的工作原理。首先,我们声明一个request函数和main生成器函数。接下来,通过调用main()创建一个迭代器it。然后,我们从调用it.next()开始。
在第一行的function * main(),在yield request('https://some-url.com')之后执行暂停。request()隐式地返回undefined,因此我们实际上生成了undefined值,但这并不重要—我们没有使用该值。
当request()函数中的fetch()调用完成时,it.next(res)将会被调用并完成下列两件事:
it继续执行;和
it将res传递给生成器函数,该函数被分配给rawResponse
最后,main()的其余部分将同步完成。
这是一个非常基础的设置,应该与promise有一些相似之处。有关yield和异步性的更详细介绍,请参阅此文。
生成器是一次性
我们不能重复使用生成器,但可以从生成器函数创建新的生成器。
function * makeGen() { yield 42; } const g1 = makeGen(); const g2 = makeGen(); g1.next(); // { value: 42, done: false } g1.next(); // { value: undefined, done: true } g1.next(); // No way to reset this! g2.next(); // { value: 42, done: false } ... const g3 = makeGen(); // Create a new generator g3.next(); // { value: 42, done: false }
无限序列
迭代器表示序列,有点像数组。所以,我们应该能够将所有迭代器表示为数组,对吧?
然而,并不是的。数组在创建时需要立即分配,而迭代器是延迟使用的。数组是迫切需要的,因为创建一个包含n个元素的数组需要首先创建/计算所有n个元素,以便将它们存储在数组中。相反,迭代器是惰性的,因为序列中的下一个值只有在使用时才会创建/计算。
因此,表示无限序列的数组在物理上是不可能的(我们需要无限内存来存储无限项!),而迭代器可以轻松地表示(而不是存储)该序列。
让我们创建一个从1到正无穷数的无穷序列。与数组不同,这并不需要无限内存,因为序列中的每个值只有在使用时才会懒散地计算出来。
function * makeInfiniteSequence() { var curr = 1; while (true) { yield curr; curr += 1; } } const is = makeInfiniteSequence(); is.next(); { value: 1, done: false } is.next(); { value: 2, done: false } is.next(); { value: 3, done: false } ... // It will never end
有趣的事实:这类似于Python生成器表达式vs列表理解。虽然这两个表达式在功能上是相同的,但是生成器表达式提供了内存优势,因为值的计算是延迟的,而列表理解则是立即计算值并创建整个列表。
推荐学习:《javascript基础教程》