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:
- Break the full run, with the ability to pause and start
- 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:
- Any data structure that has
Iterator
Interface, can beyield*
Traverse. - If the agent
Generator
Function has areturn
Statement, then you can send it to the proxyGenerator
Function 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! π