Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

The syntax of the ES6 Generator function


May 08, 2021 ES6


Table of contents


1. Introduction

Basic concepts

Generator are an asynchronous programming solution provided by ES6, 异步编程 the syntax behaves completely differently from traditional functions. This chapter details the syntax and APIs of the Generator function, and its asynchronous programming applications are described in the chapter Asynchronous Applications of the Generator function.

Generator functions have several perspectives of understanding. Syntax, you can first understand that the Generator function is a state 状态机 multiple internal states.

Executing the Generator function returns a traverser object, that is, the Generator function is a traverser object generator in addition to a state machine. The returned 遍历器对象 which can traverse each state inside the Generator function in turn.

Formally, Generator function is a normal 普通函数 but has two characteristics. One is that there is an asterisk between the function keyword and the function name, and the other is that the yield expression is used inside the function body to define different internal states (yield means "output" in English).

  1. function* helloWorldGenerator() {
  2. yield 'hello';
  3. yield 'world';
  4. return 'ending';
  5. }
  6. var hw = helloWorldGenerator();

The above code defines a Generator function, helloWorldGenerator, which has two yield expressions (hello and world) inside it, that is, the function has three states: hello, world, and return statements (end execution).

The Generator function is then called in the same way as a normal function, with a pair of parentheses after the function name. The difference is that when the Generator function is called, the function is not executed and does not return the result of the function run, but rather a pointer object to the internal state, the traverser object described in the previous chapter.

Next, you must call the next method of the traverser object so that the pointer moves to the next state. T hat is, each time the next method is called, the internal pointer starts at the head of the function or where it last stopped until the next yield expression (or return statement) is encountered. In other words, the Generator function is executed in segments, the yield expression is a tag that pauses execution, and the next method can resume execution.

  1. hw.next()
  2. // { value: 'hello', done: false }
  3. hw.next()
  4. // { value: 'world', done: false }
  5. hw.next()
  6. // { value: 'ending', done: true }
  7. hw.next()
  8. // { value: undefined, done: true }

The above code calls the next method four times in total.

On the first call, the Generator function starts executing until the first yield expression is encountered. The next method returns an object whose value property is the value of the current yield expression hello, and the value false of the done property, which indicates that the traversal is not over yet.

On the second call, the Generator function executes from where the last yield expression stopped until the next yield expression. The value property of the object returned by the next method is the value world of the current yield expression, and the value false of the done property indicates that the traversal is not over yet.

On the third call, the Generator function executes from where the last yield expression stopped until the return statement (if there is no return statement, until the end of the function). The value property of the object returned by the next method is the value of the expression immediately following the return statement (if there is no return statement, the value property's value is undefined), and the value of the done property is true, indicating that the traversal is over.

The fourth call, at which point the Generator function is already running, the next method returns the value property of the object as undefined and the done property as true. The next method is called later and returns this value.

To summarize, call the Generator function and return a traverser object, representing the internal pointer to the Generator function. L ater, each time you call the next method of the traverser object, an object with value and done properties is returned. The value property represents the value of the current internal state, the value of the expression after the yield expression, and the done property is a Boolean value that indicates whether the traversal ends.

ES6 does not specify where the asterisk between the function keyword and the function name is written. This results in the following writing passing.

  1. function * foo(x, y) { ··· }
  2. function *foo(x, y) { ··· }
  3. function* foo(x, y) { ··· }
  4. function*foo(x, y) { ··· }

Because the Generator function is still a normal function, the general writing is the third type above, the asterisk followed by the function keyword. This is also the way this book is written.

2. yield expression

Because the Generator function returns a traverser object, only calling the next method traverses the next internal state, providing a function that can be paused. yield expression is 暂停标志

The next method of the traverser object runs as follows.

(1) When a yield expression is encountered, the subsequent action is paused and the value of the expression immediately following the yield is used as the value property value of the returned object.

(2) The next time the next method is called, continue down until the next yield expression is encountered.

(3) If no new yield expression is encountered, it runs until the end of the function until the return statement, and the value of the expression after the return statement is used as the value property value of the returned object.

(4) If the function does not have a return statement, the value property value of the returned object is undefined.

Note that the expression after the yield expression is executed only when the next method is called and the internal pointer points to the statement, thus providing JavaScript with the syntax of manual Lazy Evaluation.

  1. function* gen() {
  2. yield 123 + 456;
  3. }

In the above code, the expression 123 plus 456 after yield is not evaluated immediately and will only be evaluated when the next method moves the pointer to this sentence.

Yield expressions are similar to and different from return statements. S imilarly, you can return the value of the expression that follows the statement. T he difference is that each time you encounter yield, the function pauses execution and continues back from that location the next time, while the return statement does not have the function of location memory. I n a function, only one (or one) return statement can be executed, but multiple (or more) yield expressions can be executed. N ormal functions can only return one value because they can only be executed once; Generator functions can return a series of values because there can be as many yields as anywhere. From another point of view, it can also be said that Generator generates a series of values, which is the place of its name (in English, the word generator means "generator").

The Generator function can become a simple suspended execution function without the yield expression.

  1. function* f() {
  2. console.log('执行了!')
  3. }
  4. var generator = f();
  5. setTimeout(function () {
  6. generator.next()
  7. }, 2000);

In the above code, the function f, if it is a normal function, is executed when the variable generator is assigned a value. However, function f is a Generator function that becomes executed only when the next method is called.

It is also important to note that yield expressions can only be used in Generator functions and can be used elsewhere to report errors.

  1. (function (){
  2. yield 1;
  3. })()
  4. // SyntaxError: Unexpected number

The above code uses the yield expression in a normal function, resulting in a synth error.

Here's another example.

  1. var arr = [1, [[2, 3], 4], [5, 6]];
  2. var flat = function* (a) {
  3. a.forEach(function (item) {
  4. if (typeof item !== 'number') {
  5. yield* flat(item);
  6. } else {
  7. yield item;
  8. }
  9. });
  10. };
  11. for (var f of flat(arr)){
  12. console.log(f);
  13. }

The above code also produces a syntmeal error because the argument to the forEach method is a normal function, but the yield expression is used inside (the yield expression is also used in this function, as detailed later). One way to modify this is to use the for loop between .

  1. var arr = [1, [[2, 3], 4], [5, 6]];
  2. var flat = function* (a) {
  3. var length = a.length;
  4. for (var i = 0; i < length; i++) {
  5. var item = a[i];
  6. if (typeof item !== 'number') {
  7. yield* flat(item);
  8. } else {
  9. yield item;
  10. }
  11. }
  12. };
  13. for (var f of flat(arr)) {
  14. console.log(f);
  15. }
  16. // 1, 2, 3, 4, 5, 6

In addition, if the yield expression is used in another expression, it must be placed in parentheses.

  1. function* demo() {
  2. console.log('Hello' + yield); // SyntaxError
  3. console.log('Hello' + yield 123); // SyntaxError
  4. console.log('Hello' + (yield)); // OK
  5. console.log('Hello' + (yield 123)); // OK
  6. }

The yield expression is used as a function argument or placed to the right of the assignment expression without parentheses.

  1. function* demo() {
  2. foo(yield 'a', yield 'b'); // OK
  3. let input = yield; // OK
  4. }

Relationship with the Iterator interface

As the previous chapter said, the Symbol.iterator method for any object is equal to the object's traverser generator, and calling the function returns a traverser object for that object.

Because the Generator function is the traverser generator, Generator to the Symbol.iterator object, making the object have an Iterator interface.

  1. var myIterable = {};
  2. myIterable[Symbol.iterator] = function* () {
  3. yield 1;
  4. yield 2;
  5. yield 3;
  6. };
  7. [...myIterable] // [1, 2, 3]

In the above code, Generator function is assigned to Symbol.iterator property, myIterable object has an Iterator interface that can be ... The operator traversed.

After the Generator function is executed, a traverser object is returned. The object itself also has the Symbol.iterator property, which returns itself after execution.

  1. function* gen(){
  2. // some code
  3. }
  4. var g = gen();
  5. g[Symbol.iterator]() === g
  6. // true

In the code above, gen is a Generator function that calls it to generate a traverser object g. Its Symbol.iterator property, which is also a traverser object generator, is executed and returned to itself.

2. The parameters of the next method

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

  1. function* f() {
  2. for(var i = 0; true; i++) {
  3. var reset = yield i;
  4. if(reset) { i = -1; }
  5. }
  6. }
  7. var g = f();
  8. g.next() // { value: 0, done: false }
  9. g.next() // { value: 1, done: false }
  10. g.next(true) // { value: 0, done: false }

The above code first defines a Generator function Generator that can run indefinitely, and if the next method does not have parameters, the value of the variable reset is always undefined each time it runs to the yield expression. When the next method takes a parameter true, the variable reset is reset to this parameter (i.e. true), so i is equal to -1, and the next cycle increments from -1.

This feature has important grammatical significance. T he Generator function runs from pause to resume, and its context state remains the same. W ith the parameters of the next method, there is a way to continue injecting values into the inside of the function body after the Generator function has started running. That is, you can adjust the function behavior by injecting different values from the outside to the inside at different stages of the Generator function's run.

Let's look at another example.

  1. function* foo(x) {
  2. var y = 2 * (yield (x + 1));
  3. var z = yield (y / 3);
  4. return (x + y + z);
  5. }
  6. var a = foo(5);
  7. a.next() // Object{value:6, done:false}
  8. a.next() // Object{value:NaN, done:false}
  9. a.next() // Object{value:NaN, done:true}
  10. var b = foo(5);
  11. b.next() // { value:6, done:false }
  12. b.next(12) // { value:8, done:false }
  13. b.next(13) // { value:42, done:true }

In the above code, the second time the next method is run without parameters, the value of y is equal to 2 x undefined (that is, NaN), divided by 3 and then NaN, so the value property of the returned object is equal to NaN. The next method is run without parameters for the third time, so z is equal to undefined, and the value property of the returned object is equal to 5 plus NaN plus undefined, that is, NaN.

If you provide parameters to the next method, the return results are completely different. The next method of b is returned the first time the code above calls the next method of b, and the next method is called a second time, setting the value of the previous yield expression to 12, so y equals 24 and returns y / 3 The value of is 8; the third call to the next method, setting the value of the previous yield expression to 13, so z is equal to 13, when x is equal to 5, y is equal to 24, and the value of the return statement is equal to 42.

Note that because the arguments of the next method represent the return value of the previous yield expression, passing the arguments is not valid the first time the next method is used. T he V8 engine ignores the parameters the first time the next method is used, and the parameters are valid only if they start with the next method the second time. Semantically, the first next method is used to start the traverser object, so there are no parameters.

Look at another example of entering a value inside the Generator function through the parameters of the next method.

  1. function* dataConsumer() {
  2. console.log('Started');
  3. console.log( 1. ${yield} );
  4. console.log( 2. ${yield} );
  5. return 'result';
  6. }
  7. let genObj = dataConsumer();
  8. genObj.next();
  9. // Started
  10. genObj.next('a')
  11. // 1. a
  12. genObj.next('b')
  13. // 2. b

The above code is a very intuitive example of entering a value into the Generator function each time via the next method and then printing it out.

If you want to enter a value the first time you call the next method, you can wrap another layer outside the Generator function.

  1. function wrapper(generatorFunction) {
  2. return function (...args) {
  3. let generatorObject = generatorFunction(...args);
  4. generatorObject.next();
  5. return generatorObject;
  6. };
  7. }
  8. const wrapped = wrapper(function* () {
  9. console.log( First input: ${yield} );
  10. return 'DONE';
  11. });
  12. wrapped().next('hello!')
  13. // First input: hello!

In the above code, the Generator function can't enter parameters the first time it calls the next method without wrapper wrapping a layer first.

3. for... Of loop

for...of loop automatically traverses Generator object generated when the Generator function Iterator and the next method no longer needs to be called.

  1. function* foo() {
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. yield 4;
  6. yield 5;
  7. return 6;
  8. }
  9. for (let v of foo()) {
  10. console.log(v);
  11. }
  12. // 1 2 3 4 5

The above code uses for... o f loops, showing the values of five yield expressions in turn. I t is important to note here that once the next method returns the object's done property to true, for... T he of loop aborts and does not contain the return object, so the return statement in the above code returns 6 that is not included in the for... of the loop.

Here's a take advantage of the Generator function and for... Of loop, an example of implementing the Fibonachi number column.

  1. function* fibonacci() {
  2. let [prev, curr] = [0, 1];
  3. for (;;) {
  4. yield curr;
  5. [prev, curr] = [curr, prev + curr];
  6. }
  7. }
  8. for (let n of fibonacci()) {
  9. if (n > 1000) break;
  10. console.log(n);
  11. }

As can be seen from the code above, use for... The next method is not required for the statement.

Take advantage of for... O f loops, you can write a way to traverse any object. N ative JavaScript objects do not have traversal interfaces and cannot use for... Of loops, it can be used by adding this interface to the Generator function.

  1. function* objectEntries(obj) {
  2. let propKeys = Reflect.ownKeys(obj);
  3. for (let propKey of propKeys) {
  4. yield [propKey, obj[propKey]];
  5. }
  6. }
  7. let jane = { first: 'Jane', last: 'Doe' };
  8. for (let [key, value] of objectEntries(jane)) {
  9. console.log( ${key}: ${value} );
  10. }
  11. // first: Jane
  12. // last: Doe

In the code above, the object jane native does not have an Iterator interface and cannot use for... o f traversal. A t this point, we add the traverser interface to it through the Generator function objectEntries, and we can use for... O f traversed. Another way to add a traverser interface is to add the Generator function to the symbol.iterator property of the object.

  1. function* objectEntries() {
  2. let propKeys = Object.keys(this);
  3. for (let propKey of propKeys) {
  4. yield [propKey, this[propKey]];
  5. }
  6. }
  7. let jane = { first: 'Jane', last: 'Doe' };
  8. jane[Symbol.iterator] = objectEntries;
  9. for (let [key, value] of jane) {
  10. console.log( ${key}: ${value} );
  11. }
  12. // first: Jane
  13. // last: Doe

Except for... O utside the loop, the extension operator (... ) , deconstructed assignments, and called inside the Array.from method, are traverser interfaces. This means that they can all use the Iterator object returned by the Generator function as an argument.

  1. function* numbers () {
  2. yield 1
  3. yield 2
  4. return 3
  5. yield 4
  6. }
  7. // 扩展运算符
  8. [...numbers()] // [1, 2]
  9. // Array.from 方法
  10. Array.from(numbers()) // [1, 2]
  11. // 解构赋值
  12. let [x, y] = numbers();
  13. x // 1
  14. y // 2
  15. // for...of 循环
  16. for (let n of numbers()) {
  17. console.log(n)
  18. }
  19. // 1
  20. // 2

4. Generator.prototype.throw()

Generator function returns a traverser object that has throw that throws an error outside the function body and then captures it inside the Generator function.

  1. var g = function* () {
  2. try {
  3. yield;
  4. } catch (e) {
  5. console.log('内部捕获', e);
  6. }
  7. };
  8. var i = g();
  9. i.next();
  10. try {
  11. i.throw('a');
  12. i.throw('b');
  13. } catch (e) {
  14. console.log('外部捕获', e);
  15. }
  16. // 内部捕获 a
  17. // 外部捕获 b

In the code above, the traverser object i throws two errors in a row. T he first error is captured by the catch statement inside the Generator function. i The second throw error, because the catch statement inside the Generator function has been executed and will no longer catch the error, the error is thrown out of the Generator function body and captured by the catch statement outside the function body.

The throw method can accept an argument that is received by the catch statement and is recommended to throw an instance of the Error object.

  1. var g = function* () {
  2. try {
  3. yield;
  4. } catch (e) {
  5. console.log(e);
  6. }
  7. };
  8. var i = g();
  9. i.next();
  10. i.throw(new Error('出错了!'));
  11. // Error: 出错了!(…)

Note that don't confuse the throw method of the traverser object with the global throw command. E rrors in the above code are thrown using the throw method of the traverser object, not with the throw command. The latter can only be captured by catch statements outside the function body.

  1. var g = function* () {
  2. while (true) {
  3. try {
  4. yield;
  5. } catch (e) {
  6. if (e != 'a') throw e;
  7. console.log('内部捕获', e);
  8. }
  9. }
  10. };
  11. var i = g();
  12. i.next();
  13. try {
  14. throw new Error('a');
  15. throw new Error('b');
  16. } catch (e) {
  17. console.log('外部捕获', e);
  18. }
  19. // 外部捕获 [Error: a]

The above code only captures a because the catch statement block outside the function body, after capturing the thrown a error, will not continue with the remaining statements in the try block.

If the Generator function does not have a try deployed on-premises... c atch block, then the throw method throws an error that will be external try... Catch block capture.

  1. var g = function* () {
  2. while (true) {
  3. yield;
  4. console.log('内部捕获', e);
  5. }
  6. };
  7. var i = g();
  8. i.next();
  9. try {
  10. i.throw('a');
  11. i.throw('b');
  12. } catch (e) {
  13. console.log('外部捕获', e);
  14. }
  15. // 外部捕获 a

In the code above, the Generator function g does not have try deployed on-premises... catch block, so the thrown error is caught directly by the external catch block.

If the Generator function is inside and outside, there is no try deployed... catch code block, then the program will report errors and interrupt execution directly.

  1. var gen = function* gen(){
  2. yield console.log('hello');
  3. yield console.log('world');
  4. }
  5. var g = gen();
  6. g.next();
  7. g.throw();
  8. // hello
  9. // Uncaught undefined

In the code above, after g.throw throwing an error, there is no try... Catch blocks of code can catch this error, causing the program to report errors and interrupt execution.

Errors thrown by the throw method are internally caught, provided that the next method must be executed at least once.

  1. function* gen() {
  2. try {
  3. yield 1;
  4. } catch (e) {
  5. console.log('内部捕获');
  6. }
  7. }
  8. var g = gen();
  9. g.throw(1);
  10. // Uncaught 1

In the above code, the next method was not executed once when g.throw(1) was executed. A t this point, the thrown error is not caught internally, but is thrown directly outside, causing the program to make an error. This behavior is actually very understandable, because the first execution of the next method is equivalent to starting the internal code that executes the Generator function, otherwise the Generator function has not yet started executing, when the throw method throws the wrong way and can only be thrown outside the function.

Once the throw method is captured, the next yield expression is executed. That is, the next method is executed once.

  1. var gen = function* gen(){
  2. try {
  3. yield console.log('a');
  4. } catch (e) {
  5. // ...
  6. }
  7. yield console.log('b');
  8. yield console.log('c');
  9. }
  10. var g = gen();
  11. g.next() // a
  12. g.throw() // b
  13. g.next() // c

In the above code, after the g.throw method is captured, the next method is executed automatically once, so b is printed. Y ou can also see that as long as the Generator function is on-premises, try... catch block, then the throw method of the traverser throws an error that does not affect the next traversal.

In addition, the throw command is independent of the g.throw method and does not affect each other.

  1. var gen = function* gen(){
  2. yield console.log('hello');
  3. yield console.log('world');
  4. }
  5. var g = gen();
  6. g.next();
  7. try {
  8. throw new Error();
  9. } catch (e) {
  10. g.next();
  11. }
  12. // hello
  13. // world

In the above code, the error thrown by the throw command does not affect the state of the traverser, so the next method is executed twice and the correct action is performed.

The mechanism of catching errors in this function greatly facilitates the handling of errors. M ultiple yield expressions, you can use only one try... C atch blocks of code to catch errors. If you use callback function writing to catch multiple errors, you have to write an error-handling statement inside each function, and now you can write a catch statement only once inside the Generator function.

Errors thrown outside the generator function can be caught inside the function body, and in turn, errors thrown inside the Generator function can also be caught by catch outside the function.

  1. function* foo() {
  2. var x = yield 3;
  3. var y = x.toUpperCase();
  4. yield y;
  5. }
  6. var it = foo();
  7. it.next(); // { value:3, done:false }
  8. try {
  9. it.next(42);
  10. } catch (err) {
  11. console.log(err);
  12. }

In the above code, the second next method sends a parameter 42 into the body of the function, the value is not the toUpperCase method, so a TypeError error is thrown and caught by catch outside the function body.

Once an error is thrown during Generator execution and is not captured internally, it is no longer executed. If the next method is called later, an object with a value property equal to undefined and the done property equal to true is returned, i.e. the JavaScript engine considers the Generator to be running out.

  1. function* g() {
  2. yield 1;
  3. console.log('throwing an exception');
  4. throw new Error('generator broke!');
  5. yield 2;
  6. yield 3;
  7. }
  8. function log(generator) {
  9. var v;
  10. console.log('starting generator');
  11. try {
  12. v = generator.next();
  13. console.log('第一次运行next方法', v);
  14. } catch (err) {
  15. console.log('捕捉错误', v);
  16. }
  17. try {
  18. v = generator.next();
  19. console.log('第二次运行next方法', v);
  20. } catch (err) {
  21. console.log('捕捉错误', v);
  22. }
  23. try {
  24. v = generator.next();
  25. console.log('第三次运行next方法', v);
  26. } catch (err) {
  27. console.log('捕捉错误', v);
  28. }
  29. console.log('caller done');
  30. }
  31. log(g());
  32. // starting generator
  33. // 第一次运行next方法 { value: 1, done: false }
  34. // throwing an exception
  35. // 捕捉错误 { value: 1, done: false }
  36. // 第三次运行next方法 { value: undefined, done: true }
  37. // caller done

The above code runs the next method three times, throws an error on the second run, and then ends the Generator function on the third run and is no longer executed.

5. Generator.prototype.return()

Generator function returns the traverser object, and there return that returns a given value and ends the traversal generator function.

  1. function* gen() {
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. }
  6. var g = gen();
  7. g.next() // { value: 1, done: false }
  8. g.return('foo') // { value: "foo", done: true }
  9. g.next() // { value: undefined, done: true }

In the above code, after the traverser object g calls the return method, the value property that returns the value is the parameter foo of the return method. Also, the traversal of the Generator function is terminated, the done property that returns the value is true, and the next method is called later, and the done property always returns true.

If no arguments are provided when the return method is called, the value property that returns the value is undefined.

  1. function* gen() {
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. }
  6. var g = gen();
  7. g.next() // { value: 1, done: false }
  8. g.return() // { value: undefined, done: true }

If there is a try inside the Generator function... The final block of code, and the try block is executing, then the return method causes an immediate entry into the final block of code, and the entire function does not end until it is executed.

  1. function* numbers () {
  2. yield 1;
  3. try {
  4. yield 2;
  5. yield 3;
  6. } finally {
  7. yield 4;
  8. yield 5;
  9. }
  10. yield 6;
  11. }
  12. var g = numbers();
  13. g.next() // { value: 1, done: false }
  14. g.next() // { value: 2, done: false }
  15. g.return(7) // { value: 4, done: false }
  16. g.next() // { value: 5, done: false }
  17. g.next() // { value: 7, done: true }

In the above code, after calling the return() method, the final block of code is executed, the rest of the code inside the try is not executed, and then waits until the final block is executed before returning the return value specified by the return() method.

6. Commonality of next(), throw(), return().

next() throw() return() methods are essentially the same thing and can be understood together. Their role is to get the Generator function to resume execution and replace the yield expression with a different statement.

next() is to replace the yield expression with a value.

  1. const g = function* (x, y) {
  2. let result = yield x + y;
  3. return result;
  4. };
  5. const gen = g(1, 2);
  6. gen.next(); // Object {value: 3, done: false}
  7. gen.next(1); // Object {value: 1, done: true}
  8. // 相当于将 let result = yield x + y
  9. // 替换成 let result = 1;

In the above code, the second next(1) method is equivalent to replacing the yield expression with a value of 1. If the next method does not have parameters, it is equivalent to replacing it with undefined.

throw() is to replace the yield expression with a throw statement.

  1. gen.throw(new Error('出错了')); // Uncaught Error: 出错了
  2. // 相当于将 let result = yield x + y
  3. // 替换成 let result = throw(new Error('出错了'));

return() is the replacement of the yield expression with a return statement.

  1. gen.return(2); // Object {value: 2, done: true}
  2. // 相当于将 let result = yield x + y
  3. // 替换成 let result = return 2;

7. yield-expression

If inside the Generator function, call another Generator function. The traversal needs to be done manually within the function body of the former.

  1. function* foo() {
  2. yield 'a';
  3. yield 'b';
  4. }
  5. function* bar() {
  6. yield 'x';
  7. // 手动遍历 foo()
  8. for (let i of foo()) {
  9. console.log(i);
  10. }
  11. yield 'y';
  12. }
  13. for (let v of bar()){
  14. console.log(v);
  15. }
  16. // x
  17. // a
  18. // b
  19. // y

In the code above, foo and bar are generator functions, and calling foo in bar requires manual traversal of foo. If you have more than one Generator function nested, it can be cumbersome to write.

ES6 provides a yield-expression as a work-through to execute another Generator function in one Generator function.

  1. function* bar() {
  2. yield 'x';
  3. yield* foo();
  4. yield 'y';
  5. }
  6. // 等同于
  7. function* bar() {
  8. yield 'x';
  9. yield 'a';
  10. yield 'b';
  11. yield 'y';
  12. }
  13. // 等同于
  14. function* bar() {
  15. yield 'x';
  16. for (let v of foo()) {
  17. yield v;
  18. }
  19. yield 'y';
  20. }
  21. for (let v of bar()){
  22. console.log(v);
  23. }
  24. // "x"
  25. // "a"
  26. // "b"
  27. // "y"

Let's look at another example of comparison.

  1. function* inner() {
  2. yield 'hello!';
  3. }
  4. function* outer1() {
  5. yield 'open';
  6. yield inner();
  7. yield 'close';
  8. }
  9. var gen = outer1()
  10. gen.next().value // "open"
  11. gen.next().value // 返回一个遍历器对象
  12. gen.next().value // "close"
  13. function* outer2() {
  14. yield 'open'
  15. yield* inner()
  16. yield 'close'
  17. }
  18. var gen = outer2()
  19. gen.next().value // "open"
  20. gen.next().value // "hello!"
  21. gen.next().value // "close"

In the example above, outer2 uses yield, and outer1 does not. As a result, outer1 returns a traverser object, and outer2 returns the internal value of the traverser object.

From a syntax point of view, if the yield expression is followed by a traverser object, you need to add an asterisk after the yield expression to indicate that it returns a traverser object. This is called a yield-expression.

  1. let delegatedIterator = (function* () {
  2. yield 'Hello!';
  3. yield 'Bye!';
  4. }());
  5. let delegatingIterator = (function* () {
  6. yield 'Greetings!';
  7. yield* delegatedIterator;
  8. yield 'Ok, bye.';
  9. }());
  10. for(let value of delegatingIterator) {
  11. console.log(value);
  12. }
  13. // "Greetings!
  14. // "Hello!"
  15. // "Bye!"
  16. // "Ok, bye."

In the code above, delegatingIterator is the agent, and delegatedIterator is the agent. B ecause the value obtained by the yield-delegatedIterator statement is a traverser, it is represented by an asterisk. The result is a traversal that traverses multiple Generator functions with a recursive effect.

The Generator function after yield (when there is no return statement) is equivalent to deploying a for... Of loop.

  1. function* concat(iter1, iter2) {
  2. yield* iter1;
  3. yield* iter2;
  4. }
  5. // 等同于
  6. function* concat(iter1, iter2) {
  7. for (var value of iter1) {
  8. yield value;
  9. }
  10. for (var value of iter2) {
  11. yield value;
  12. }
  13. }

The code above states that the Generator function after yield (when there is no return statement) is just for... A short-form form of, can completely replace the former with the latter. Conversely, when there is a return statement, you need to get the value of the return statement in the form of var value s yield iterator.

If the yield is followed by an array, the array members are traversed because the array natively supports the traverser.

  1. function* gen(){
  2. yield* ["a", "b", "c"];
  3. }
  4. gen().next() // { value:"a", done:false }

In the code above, if the yield command is followed by an asterisk, the entire array is returned, and the asterisk is added to indicate that the array's traverser object is returned.

In fact, any data structure that has an Iterator interface can be traversed by yield.

  1. let read = (function* () {
  2. yield 'hello';
  3. yield* 'hello';
  4. })();
  5. read.next().value // "hello"
  6. read.next().value // "h"

In the above code, the yield expression returns the entire string, and the yield statement returns a single character. Because the string has an Iterator interface, it is traversed by yield.

If the generator function of the proxy has a return statement, you can return data to the Generator function that is proxying it.

  1. function* foo() {
  2. yield 2;
  3. yield 3;
  4. return "foo";
  5. }
  6. function* bar() {
  7. yield 1;
  8. var v = yield* foo();
  9. console.log("v: " + v);
  10. yield 4;
  11. }
  12. var it = bar();
  13. it.next()
  14. // {value: 1, done: false}
  15. it.next()
  16. // {value: 2, done: false}
  17. it.next()
  18. // {value: 3, done: false}
  19. it.next();
  20. // "v: foo"
  21. // {value: 4, done: false}
  22. it.next()
  23. // {value: undefined, done: true}

The above code will have output on the screen the fourth time the next method is called, because the return statement of the function foo provides a return value to the function bar.

Let's look at another example.

  1. function* genFuncWithReturn() {
  2. yield 'a';
  3. yield 'b';
  4. return 'The result';
  5. }
  6. function* logReturned(genObj) {
  7. let result = yield* genObj;
  8. console.log(result);
  9. }
  10. [...logReturned(genFuncWithReturn())]
  11. // The result
  12. // 值为 [ 'a', 'b' ]

In the code above, there are two traversals. T he first is the traverser object returned by the extended operator traversal function logReturned, and the second is the traverser object returned by the yield statement traversal function genFuncWithReturn. T he effect of these two traversals is superimposed and eventually manifests it as the traverser object returned by the extended operator traversal function genFuncWithReturn. T herefore, the final data expression results in a value equal to 'a', 'b'. However, the return value of the return statement of the function genFuncWithReturn, The result, is returned to the result variable inside the function logReturned, so there is terminal output.

The yield command makes it easy to remove all members of a nested array.

  1. function* iterTree(tree) {
  2. if (Array.isArray(tree)) {
  3. for(let i=0; i < tree.length; i++) {
  4. yield* iterTree(tree[i]);
  5. }
  6. } else {
  7. yield tree;
  8. }
  9. }
  10. const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
  11. for(let x of iterTree(tree)) {
  12. console.log(x);
  13. }
  14. // a
  15. // b
  16. // c
  17. // d
  18. // e

Because of the extended operator ... The Iterator interface is called by default, so this function above can also be used for tiles for nested arrays.

  1. [...iterTree(tree)] // ["a", "b", "c", "d", "e"]

Here's a slightly more complex example of traversing a full binary tree with a yield statement.

  1. // 下面是二叉树的构造函数,
  2. // 三个参数分别是左树、当前节点和右树
  3. function Tree(left, label, right) {
  4. this.left = left;
  5. this.label = label;
  6. this.right = right;
  7. }
  8. // 下面是中序(inorder)遍历函数。
  9. // 由于返回的是一个遍历器,所以要用generator函数。
  10. // 函数体内采用递归算法,所以左树和右树要用yield*遍历
  11. function* inorder(t) {
  12. if (t) {
  13. yield* inorder(t.left);
  14. yield t.label;
  15. yield* inorder(t.right);
  16. }
  17. }
  18. // 下面生成二叉树
  19. function make(array) {
  20. // 判断是否为叶节点
  21. if (array.length == 1) return new Tree(null, array[0], null);
  22. return new Tree(make(array[0]), array[1], make(array[2]));
  23. }
  24. let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
  25. // 遍历二叉树
  26. var result = [];
  27. for (let node of inorder(tree)) {
  28. result.push(node);
  29. }
  30. result
  31. // ['a', 'b', 'c', 'd', 'e', 'f', 'g']

8. Generator function as an object property

If the property of an object is a Generator function, you can write it in the following form.

  1. let obj = {
  2. * myGeneratorMethod() {
  3. ···
  4. }
  5. };

In the code above, the myGeneratorMethod property is preceded by an asterisk indicating that the property is a Generator function.

Its full form is as follows, and the above writing is equivalent.

  1. let obj = {
  2. myGeneratorMethod: function* () {
  3. // ···
  4. }
  5. };

9. This of the Generator function

The Generator function always returns a traverser, which ES6 states is an instance of the Generator function and inherits the Generator object prototype the Generator function.

  1. function* g() {}
  2. g.prototype.hello = function () {
  3. return 'hi!';
  4. };
  5. let obj = g();
  6. obj instanceof g // true
  7. obj.hello() // 'hi!'

The above code indicates that the generator function g returns the traverser obj, which is an instance of g, and inherits g.prototype. However, if you treat g as a normal constructor, it does not work because g always returns a traverser object, not this object.

  1. function* g() {
  2. this.a = 11;
  3. }
  4. let obj = g();
  5. obj.next();
  6. obj.a // undefined

In the code above, the Generator function g adds a property a to the object, but the obj object does not get the property.

Generator functions can't be used with new commands, between errors.

  1. function* F() {
  2. yield this.x = 2;
  3. yield this.y = 3;
  4. }
  5. new F()
  6. // TypeError: F is not a constructor

In the code above, the new command is used with constructor F, and the result is an error because F is not a constructor.

So, is there a way for the Generator function to return a normal instance of the object, both with the next method and with a normal this?

Here's a work-through. F irst, an empty object is generated that binds this inside the Generator function using the call method. Thus, after the constructor is called, the empty object is an instance object of the Generator function.

  1. function* F() {
  2. this.a = 1;
  3. yield this.b = 2;
  4. yield this.c = 3;
  5. }
  6. var obj = {};
  7. var f = F.call(obj);
  8. f.next(); // Object {value: 2, done: false}
  9. f.next(); // Object {value: 3, done: false}
  10. f.next(); // Object {value: undefined, done: true}
  11. obj.a // 1
  12. obj.b // 2
  13. obj.c // 3

In the above code, first the this object inside F binds the obj object, then calls it and returns an Iterator object. T his object executes the next method three times (because there are two yield expressions inside F) to complete the operation of all code inside F. At this point, all internal properties are bound to the obj object, so the obj object becomes an instance of F.

In the above code, the traverser object f is executed, but the resulting object instance is obj, is there any way to unify the two objects?

One way to do this is to replace obj with F.prototype.

  1. function* F() {
  2. this.a = 1;
  3. yield this.b = 2;
  4. yield this.c = 3;
  5. }
  6. var f = F.call(F.prototype);
  7. f.next(); // Object {value: 2, done: false}
  8. f.next(); // Object {value: 3, done: false}
  9. f.next(); // Object {value: undefined, done: true}
  10. f.a // 1
  11. f.b // 2
  12. f.c // 3

Then change F to a constructor and you can execute a new command on it.

  1. function* gen() {
  2. this.a = 1;
  3. yield this.b = 2;
  4. yield this.c = 3;
  5. }
  6. function F() {
  7. return gen.call(gen.prototype);
  8. }
  9. var f = new F();
  10. f.next(); // Object {value: 2, done: false}
  11. f.next(); // Object {value: 3, done: false}
  12. f.next(); // Object {value: undefined, done: true}
  13. f.a // 1
  14. f.b // 2
  15. f.c // 3

Meaning

Generator and the state machine

Generator the best structure for implementing state machines. For example, clock function is a state machine.

  1. var ticking = true;
  2. var clock = function() {
  3. if (ticking)
  4. console.log('Tick!');
  5. else
  6. console.log('Tock!');
  7. ticking = !ticking;
  8. }

The clock function in the code above has two states (Tick and Tock), which changes every time you run. This function, if implemented with Generator, is like this.

  1. var clock = function* () {
  2. while (true) {
  3. console.log('Tick!');
  4. yield;
  5. console.log('Tock!');
  6. yield;
  7. }
  8. };

Comparing the Generator implementation above with the ES5 implementation, you can see that there are fewer external variables ticking to hold the state, which makes it cleaner, safer (the state is not illegally tampered with), more in line with the idea of functional programming, and more elegant in writing. Generator can save state without external variables because it contains a state information about whether it is currently on hold.

Generator and co-program

协程 (coroutine) are ways 程序运行 which programs run and can be understood “协作的线程” or “协作的函数” C o-programs can be 单线程 single-threaded and 多线程 The former is a special sub-routine, while the latter is a special thread.

(1) The difference between the co-program and the sub-routine

The “子例程” method of "last-in, first-out" execution ends the execution of the parent function only when the called child function is fully executed. 堆栈式 U nlike concords, multiple threads (in the case of a single thread, that is, multiple functions) can execute in parallel, but only one thread (or function) is running, the other threads (or functions) are suspended, and execution rights can be exchanged between threads (or functions). T hat is, if one thread (or function) executes in half, execution can be suspended, and execution is handed over to another thread (or function) until execution is retracted later. This thread (or function) that can execute in parallel and exchange execution rights is called a co-program.

From an implementation point of view, in memory, sub-routines use only one stack, and the co-program is that there are multiple stacks at the same time, but only one stack is in a running state, that is, the co-program is at the expense of multi-occupancy memory, to achieve multi-task parallel.

(2) The difference between the co-program and the normal thread

It is not difficult to see that co-programs are suitable for multitasing environments. I n this sense, it is similar to a normal thread, has its own execution context, and can share global variables. T hey differ in that multiple threads can be running at the same time, but only one co-program can run and the other co-programs are paused. In addition, ordinary threads are preemptive, in the end which threads get resources first, must be determined by the running environment, but the co-program is cooperative, the executive power is allocated by the co-program itself.

Because JavaScript is a single-threaded language, only one call stack can be maintained. A fter the introduction of co-programs, each task can maintain its own call stack. T he biggest benefit of this is that when you throw an error, you can find the original call stack. Not as much as the callback function for asynchronous operations, the original call stack ends long ago if something goes wrong.

The Generator function is an ES6-to-co-program implementation, but it is an incomplete implementation. T he Generator function is called a "semi-coroutine", which means that only the caller of the Generator function can return the execution of the program to the Generator function. If the co-execution is full, any function can have the suspended co-execution continue.

If you think of the Generator function as a co-program, you can write multiple tasks that require collaboration as Generator functions that exchange control with yield expressions.

Generator and context

When JavaScript code runs, a global context 上下文环境 known as a running environment) is created that contains all the current variables and objects. Then, when a function (or block-level code) is executed, it produces a context in which the function runs and becomes the context of the current (active), creating a stack of contexts in the context.

This stack is “后进先出” the resulting context is first executed, exiting the stack, and then executing the context that completes its lower layer until all code execution is complete and the stack is emptied.

This is not the case with the Generator function, which executes the resulting context and temporarily exits the stack as soon as the yield command is encountered, but does not disappear, with all variables and objects frozen in the current state. When the next command is executed on it, the context rejoins the call stack, freezing variables and objects to resume execution.

  1. function* gen() {
  2. yield 1;
  3. return 2;
  4. }
  5. let g = gen();
  6. console.log(
  7. g.next().value,
  8. g.next().value,
  9. );

In the above code, the first time g.next() is executed, the context of the Generator function gen is added to the stack, which starts running the code inside the gen. W hen yield 1 is encountered, the gen context exits the stack and the internal state freezes. The second time g.next() is executed, the gen context rejoins the stack, becomes the current context, and resumes execution.

11. Application

Generator pause 暂停函数 to return the value of any expression. This feature allows Generator to have a variety of scenarios.

(1) The simultaneous expression of asynchronous operations

Generator effect of the Generator function's pause on execution means that asynchronous operations can be written into yield expression and then executed later when the next method is called. T his is actually equivalent to not having to write a callback function, because subsequent operations of asynchronous operations can be placed under the yield expression and wait until the next method is called anyway. Therefore, an important practical significance of generator functions is to handle asynchronous operations and override callback functions.

  1. function* loadUI() {
  2. showLoadingScreen();
  3. yield loadUIDataAsynchronously();
  4. hideLoadingScreen();
  5. }
  6. var loader = loadUI();
  7. // 加载UI
  8. loader.next()
  9. // 卸载UI
  10. loader.next()

In the above code, the first time a loadUI function is called, it is not executed and only one traverser is returned. T he next time you call the next method on the traverser, the Loading interface is displayed and the data is loaded asynchronously . W hen the data load is complete and the next method is used again, the Loading interface is hidden. As you can see, the benefit of this writing is that all the logic of the Loading interface is encapsulated in a function that is very clear in order.

Ajax is a typical asynchronous operation that deploys Ajax operations through the Generator function and can be expressed in a synchronized manner.

  1. function* main() {
  2. var result = yield request("http://some.url");
  3. var resp = JSON.parse(result);
  4. console.log(resp.value);
  5. }
  6. function request(url) {
  7. makeAjaxCall(url, function(response){
  8. it.next(response);
  9. });
  10. }
  11. var it = main();
  12. it.next();

The main function of the code above is to get the data through the Ajax operation. A s you can see, with the exception of one more yield, it is almost exactly the same as the synchronous operation. Note that the next method in the makeAjaxCall function must be added to the response argument, because the yield expression itself has no value and is always equal to undefined.

Here's another example of reading a text file line by line through the Generator function.

  1. function* numbers() {
  2. let file = new FileReader("numbers.txt");
  3. try {
  4. while(!file.eof) {
  5. yield parseInt(file.readLine(), 10);
  6. }
  7. } finally {
  8. file.close();
  9. }
  10. }

The above code opens the text file and uses the yield expression to manually read the file line by line.

(2) Control flow management

If a multi-step operation is time-consuming, using a callback function, it might be written as below.

  1. step1(function (value1) {
  2. step2(value1, function(value2) {
  3. step3(value2, function(value3) {
  4. step4(value3, function(value4) {
  5. // Do something with value4
  6. });
  7. });
  8. });
  9. });

Use Promise to rewrite the code above.

  1. Promise.resolve(step1)
  2. .then(step2)
  3. .then(step3)
  4. .then(step4)
  5. .then(function (value4) {
  6. // Do something with value4
  7. }, function (error) {
  8. // Handle any error from step1 through step4
  9. })
  10. .done();

The above code has changed the callback function to a straight-line execution, but adds a lot of Promise syntax. The Generator function can further improve the code run process.

  1. function* longRunningTask(value1) {
  2. try {
  3. var value2 = yield step1(value1);
  4. var value3 = yield step2(value2);
  5. var value4 = yield step3(value3);
  6. var value5 = yield step4(value4);
  7. // Do something with value4
  8. } catch (e) {
  9. // Handle any error from step1 through step4
  10. }
  11. }

Then, use a function to automate all steps in order.

  1. scheduler(longRunningTask(initialValue));
  2. function scheduler(task) {
  3. var taskObj = task.next(task.value);
  4. // 如果Generator函数未结束,就继续调用
  5. if (!taskObj.done) {
  6. task.value = taskObj.value
  7. scheduler(task);
  8. }
  9. }

Note that the above approach is only suitable for synchronization operations, i.e. all tasks must be synchronized and not asynchronous. B ecause as soon as the code here gets a return value, it continues down without determining when the asynchronous operation will complete. If you want to control the asynchronous operation flow, see the chapter Asynchronous Operations later.

Below, take advantage of for... The of loop automatically executes the characteristics of the yield command in turn, providing a more general approach to control flow management.

  1. let steps = [step1Func, step2Func, step3Func];
  2. function* iterateSteps(steps){
  3. for (var i=0; i< steps.length; i++){
  4. var step = steps[i];
  5. yield step();
  6. }
  7. }

In the above code, the array steps encapsulate multiple steps of a task, and the Generator function iterateSteps adds the yield command to those steps in turn.

After you break a task down into steps, you can also break up the project into multiple tasks that are performed in turn.

  1. let jobs = [job1, job2, job3];
  2. function* iterateJobs(jobs){
  3. for (var i=0; i< jobs.length; i++){
  4. var job = jobs[i];
  5. yield* iterateSteps(job.steps);
  6. }
  7. }

In the above code, array jobs encapsulate multiple tasks for a project, and the Generator function iterateJobs adds yield commands to those tasks in turn.

Finally, you can use for... The of loop performs all the steps of all tasks in turn at once.

  1. for (var step of iterateJobs(jobs)){
  2. console.log(step.id);
  3. }

Again, the above practice can only be used in cases when all steps are synchronized operations and there can be no asynchronous steps. If you want to perform asynchronous steps in turn, you must use the methods described in the later chapter of Asynchronous Operations.

for... Of's nature is a while loop, so the above code essentially executes the following logic.

  1. var it = iterateJobs(jobs);
  2. var res = it.next();
  3. while (!res.done){
  4. var result = res.value;
  5. // ...
  6. res = it.next();
  7. }

(3) Deploy the Iterator interface

The Generator function allows you to deploy the Iterator object.

  1. function* iterEntries(obj) {
  2. let keys = Object.keys(obj);
  3. for (let i=0; i < keys.length; i++) {
  4. let key = keys[i];
  5. yield [key, obj[key]];
  6. }
  7. }
  8. let myObj = { foo: 3, bar: 7 };
  9. for (let [key, value] of iterEntries(myObj)) {
  10. console.log(key, value);
  11. }
  12. // foo 3
  13. // bar 7

In the above code, myObj is a normal object, and through the IterEntries function, there is an Iterator interface. That is, you can deploy the next method on any object.

The following is an example of deploying an Iterator interface to an array, although the array natively has this interface.

  1. function* makeSimpleGenerator(array){
  2. var nextIndex = 0;
  3. while(nextIndex < array.length){
  4. yield array[nextIndex++];
  5. }
  6. }
  7. var gen = makeSimpleGenerator(['yo', 'ya']);
  8. gen.next().value // 'yo'
  9. gen.next().value // 'ya'
  10. gen.next().done // true

(4) as a data structure

Generator can be 数据结构 数组结构 data structure, or rather as an array structure, because the Generator function can return a series of values, which means that it can provide an array-like interface to any expression.

  1. function* doStuff() {
  2. yield fs.readFile.bind(null, 'hello.txt');
  3. yield fs.readFile.bind(null, 'world.txt');
  4. yield fs.readFile.bind(null, 'and-such.txt');
  5. }

The above code returns three functions in turn, but because the Generator function is used, it is able to process the three returned functions as you would with arrays.

  1. for (task of doStuff()) {
  2. // task是一个函数,可以像回调函数那样使用它
  3. }

In fact, if expressed in ES5, it is perfectly possible to simulate this use of Generator with arrays.

  1. function doStuff() {
  2. return [
  3. fs.readFile.bind(null, 'hello.txt'),
  4. fs.readFile.bind(null, 'world.txt'),
  5. fs.readFile.bind(null, 'and-such.txt')
  6. ];
  7. }

The function above can be used with exactly the same for... o f loop processing! By comparison, it is not difficult to see that Generator makes data or operations with array-like interfaces.