This is the 19th day of my participation in the August More Text Challenge

πŸ“’ Hi, my name is Xiao Cheng, and this article will take you through the generators in ES6.

Writing in the front

In the previous article, we had a deep understanding of the principles and effects of iterators. In this article, we’ll take a closer look at generators that are closely related to iterators.

Generators have this description

Little Red Book: Generators are an extremely flexible addition to ES6, with the ability to pause and resume code execution within a function block

Generator functions are an asynchronous programming solution provided by ES6

From the above two paragraphs, we can see that generators have at least two functions:

  1. Break the full run, with the ability to pause and start
  2. Resolving asynchronous operations

How does a generator do this

An example of a generator

Let’s start with an example

Here is an example of a for loop that prints the current index on each loop. This code simply generates the numbers 0-5

for (let i = 0; i <= 5; i++) {
    console.log(i);
}
// Output 0, 1, 2, 3, 4, 5
Copy the code

Let’s see how this is implemented using a generator function. Okay

function* generatorForLoop(num) {
    for (let i = 0; i <= num; i ++) {
        yield console.log(i); }}const gen = generatorForLoop(5);
gen.next(); / / 0
gen.next(); / / 1
gen.next(); / / 2
gen.next(); / / 3
gen.next(); / / 4
gen.next(); / / 5
Copy the code

As you can see, only by calling the next method does the execution proceed downwards, rather than producing all the values at once. This is the simplest possible generator. In some cases, this feature can be a killer

The basic concept

1. Function declaration

The form of a generator is a function whose name is preceded by an asterisk * to indicate that it is a generator.

// Function declaration
function * generator () {}
// Function expression
let generator = function* () {}
Copy the code

When defining a generator, the asterisk precedes the function name, but there is no specific requirement for the location, and you don’t need to worry about who is next to it

Wherever you can define a function, you can define a generator.

It is important to note that arrow functions cannot be used to define generators

2. Yield expression

Inside the function body, we use yield expressions to define different internal states. Let’s look at a piece of code

function* helloWorld() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
Copy the code

In the above code, we define a generator function helloWorld with two yield expressions and three states: hello, world, and return statements

At the heart of a generator, this explanation alone probably fails to understand what yield does and how it can be used

Now let’s expand on the yield keyword

First, it is somewhat similar to the return keyword in that a return statement returns a value after a function call, but nothing can be done after a return statement

You can see that the code after the first return statement in the compiler grays out, indicating that it has not taken effect. But the way yield works is different, so how does yield work

Note: The yield keyword can only be used inside a generator function; using it elsewhere throws an error

First, the generator function returns an iterator object. The next state is iterated only by calling the next method, and yield is a pause

In the above code, we first declare a generator function that uses the myR variable to receive the return value of the generator function, the traverser object described above, while it is paused.

When the next method is called, the execution begins and the yield expression is encountered. The yield expression is paused as the value of the returned object, so the value in the first myr.next () is 8

When the next method is called again, the execution continues until yield is reached, and the rest is the same

Note that the expression following the yield expression is executed only if the next method is called with an internal pointer to that statement

function* gen() {
  yield  123 + 456;
}
Copy the code

In the code above, for example, the expression 123 + 456 following yield is not evaluated immediately, but only when the next method moves the pointer to that line.

Therefore, it can be understood that return is an end and yield is a stop

3. Must yield statements be used?

It is possible to do without a yield expression in a generator function, but the generator nature remains, so it becomes a pure deferred function, executed only on the next method of the traverser object calling the function

function* hello() {
    console.log('Execute now');
}
// Generate a traverser object
let generator = hello()
setTimeout(() = > {
    // Start executing
    generator.next()
}, 2000)
Copy the code

4. Pay attention to

The yield expression must be enclosed in parentheses if it is used in another expression

console.log('Hello' + (yield 123)); // OK
Copy the code

Yield expressions can be used as function arguments without parentheses

foo(yield 'a')
Copy the code

How to understand Generator functions as state machines?

Ruan Yifeng’s ES6 books have such an understanding of generator functions

Generator functions can be understood in many ways. Syntactically, the Generator function is a state machine that encapsulates multiple internal states.

A Generator is a state machine. A state machine is a Generator.

This has something to do with JavaScript state patterns

State mode: When the internal state of an object changes, it causes its behavior to change, which appears to change the object

When you look at these definitions, it’s obvious you know what every word means, but you don’t know what they mean when you put them together

So before we panic, let’s see what a state pattern is, just write a state machine, okay

Let’s take the example of a washing machine, press the power button to turn it on, press the power button to turn it off, and let’s do that first

let switches = (function () {
    let state = "off";
    return function () {
        if (state === "off") {
            console.log("Turn on the washing machine");
            state = "on";
        } else if (state === "on") {
            console.log("Turn off the washing machine");
            state = "off"; }}}) ();Copy the code

In the above code, return a function with the state state stored inside the function, by pressing the power key to call the switches function each time.

Now let’s change the requirements. There is a button on the washing machine to adjust the mode. Every time you press the button, change the mode

We can do the same thing with if-else

let switches = (function () {
    let state = "Fast";
    return function () {
        if (state === "Fast") {
            console.log("Washing mode");
            state = "Washing";
        } else if (state === "Washing") {
            console.log("Rinse mode");
            state = "Rinse";
        } else if (state === "Rinse") {
            console.log("Dehydration mode");
            state = "Dehydration";
        } else if (state === "Dehydration") {
            console.log("Fast Mode");
            state = "Fast"; }}}) ();Copy the code

It gets more and more complicated, and as you get more and more patterns, you get more and more if-else statements, and your code gets harder to read, and you might say you can do that with switch-case, and you can do that, but you can do that. Can we do it without a judgment statement? So let’s go back to our original definition

State mode: When the internal state of an object changes, it causes its behavior to change, which appears to change the object

Gee, think about it, isn’t it the washing machine that needs to achieve state change, behavior change? This can be done in state mode, where we introduce a generator function that controls the state to change its behavior

The prototype approach is too complex and redundant to show

const fast = function () {
    console.log("Fast Mode");
}
const wash = function () {
    console.log("Washing mode");
}
const rinse = function () {
    console.log("Rinse mode");
}
const dehydration = function () {
    console.log("Dehydration mode");
}

function* models() {
    let i = 0,
        fn, len = arguments.length;
    while (true) {
        fn = arguments[i++]
        yield fn()
        if (i === len) {
            i = 0; }}}const exe = models(fast, wash, rinse, dehydration); // Discharge according to pattern sequence
Copy the code

In the above code we simply call the next method on each press to switch to the next state

So why is generator a state machine? When a Generator function is called to retrieve an iterator, the state machine is in its initial state. After the iterator calls the next method, it jumps to the next state and executes the code for that state. When a return or the last yield is encountered, the final state is entered. The state machine with Generator is the best structure.

Next Pass parameters

Another strength of generators is their built-in message input and output capabilities, which rely on yield and next methods

The yield expression itself returns no value, or always returns undefined. The next method can take an argument that is treated as the return value of the previous yield expression.

Semantically, the first next method is used to start the traverser object, so it doesn’t take arguments.

Let’s look at an example

function* foo(x) {
    let y = x * (yield)
    return y
}
const it = foo(6)
it.next()
let res = it.next(7)
console.log(res.value) / / 42
Copy the code

In the above code, calling foo returns an iterator object it, passing 6 to X, calls next on the iterator object, starts the iterator object, and stops at the first yield,

The next method is called again, passing in the argument 7 as the return value of the previous yield expression (yield of x), until the next yield or return

Now the death begins

In the example above, what would happen if no arguments were passed? The second time I run the next method with no arguments, the result is that y is equal to 6 * undefined which is NaN so the value property of the returned object is also NaN

Let’s change it again

In the original example, we said that the first next was used to start the traverser object, so what happens if we pass in the argument?

Passing arguments in this way is invalid because we say that the arguments to the next method represent the return value of the previous yield expression.

The V8 engine simply ignores the parameters when the next method is first used

Relationship to the Iterator interface

As we saw in the previous article, the symbol. iterator method of an object is equal to the iterator generator function of that object, which returns an iterator object

In this article we learned that generator functions are ergoerator generators, so any ideas?

We can implement the Iterator interface by assigning the generator to the object’s symbol. iterator property

let myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
}

[...myIterable] / / [1, 2, 3]
Copy the code

Premature termination generator

Each traverser object returned by a generator function has a next method, as well as optional return and throw methods

Let’s look at the return method first

return

The return method forces the generator into a closed state. The value provided to the return method is the value of the terminating iterator object, that is, the state of the returned object is true and the value is the value passed in. So let’s verify that

function* genFn() {
    for (const x of [1.2.3]) {
        yield x
    }
}
// Create the traverser object g
const g = genFn()
// End manually
console.log(g.return('the end'))
Copy the code

In the code above, {value: “end “, done: true} is printed, as expected, and we terminate the generator by calling return directly after generating the traverser object

If a generator function has a try inside it… Finally block, and a try block is being executed, then the return() method causes the finally block to be entered immediately, and the whole function to end after execution.

function* genFn() {
    try {
        yield 111
    } finally {
        console.log('I'm in finally');
        yield 999}}// Create the traverser object g
const g = genFn()
/ / start
g.next()
console.log(g.return('the end'))
Copy the code

In the above code, executing the next function causes the try block to begin execution, and calling the return method begins execution of the finally block, waits for execution to complete, and then returns the return value specified by the return method

throw

The throw() method injects a supplied error into the generator object when paused. If the error is not handled, the generator is shut down

In a lot of data are said to be very complex, in fact, very simple:

If there is a mistake, you will give me a catch to deal with it, or you will quit. That is bullying

function* gen(){
    console.log("state1");
    let state1 = yield "state1";
    console.log("state2");
    let state2 = yield "state2";
    console.log("end");
}
let g = gen();
g.next();
g.throw();
Copy the code

In the above code, the throw method raises an error that is not handled and therefore exits directly, so the above code simply prints state1 and then an error

Note: You can pass arguments to the throw method to explain the error

g.throw(new Error('Wrong! '))
Copy the code

Next (), throw(), return()

All three methods of iterating through the object have been covered up to this point, and while their functions are different or completely unrelated, they do essentially do the same thing: “Replace yield expressions with statements.”

Next replaces the yield expression with a value

A throw replaces a yield expression with a throw statement

gen.throw(new Error('Wrong')); // Uncaught Error: There is an Error
// let result = yield x + y
// ζ›Ώζ’ζˆ let result = throw(new Error('ε‡Ίι”™δΊ†'));
Copy the code

Return replaces a yield expression with a return statement

Yield expression *

Yield, with an asterisk, enhances the behavior of yield by iterating over an iterable to produce one value at a time, also known as delegate iteration. In this way, multiple generators can be linked together.

function * anotherGenerator(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

function * generator(i) {
  yield* anotherGenerator(i);
}

var gen = generator(1);

gen.next().value; / / 2
gen.next().value; / / 3
gen.next().value; / / 4
Copy the code

A few notes:

  1. Any data structure that hasIteratorInterface, can beyield*Traverse.
  2. If the agentGeneratorFunction has areturnStatement, then you can send it to the proxyGeneratorFunction returns data.

Use yield* to implement recursive algorithms

Implement recursive algorithms, and this is where yield* is most useful, where the generator can generate itself

function* nTimes(n) {
    if (n > 0) {
        yield* nTimes(n - 1);
        yield n - 1; }}for (const x of nTimes(3)) {
    console.log(x);
}
/ / 0 1 2
Copy the code

In the code above, each generator first produces each value from the newly created generator object, and then an integer.

The resources

What is a JavaScript generator? How do I use generators?

Ruan Yifeng teacher the syntax of Generator functions

JavaScript Advanced Programming Edition 4


Previous article: Iterators in JavaScript

That concludes this article, and the core application of asynchronous coding patterns and callbacks for generators will be summarized in the next article.

Thank you very much for reading, welcome to put forward your opinion, if you have any questions, please point out, thank you! 🎈