Diving Deeper With ES6 Generators

Due to my limited ability, there are inevitably mistakes and omissions in translation, so PLEASE correct the issue

ES6 Generators: Complete series

  1. The Basics Of ES6 Generators
  2. Diving Deeper With ES6 Generators
  3. Going Async With ES6 Generators
  4. Getting Concurrent With ES6 Generators

If you are still not familiar with ES6 generators, it is recommended that you read the first article in this series, “Part 1: A Basic guide to ES6 Generators” and practice the code snippets. Once you feel comfortable with the basics, we can begin to delve into some of the details of the Generator function.

Error handling

The most powerful part of the ES6 generators design is the semantic understanding that the code in the generator is synchronous, even though the external iteration controller is executed asynchronously.

Generators that is, you can use a simple error-handling technique to treat generators as fault-tolerant functions, otherwise known as try… The mechanism of the catch.

Such as:

function *foo() { try { var x = yield 3; console.log( "x: " + x ); // may never get here! } catch (err) { console.log( "Error: " + err ); }}Copy the code

Although the foo generator function in the above example will pause after the yield 3 expression, and possibly for any length of time, if an error is passed inside the generator function, the try inside the generator function… The catch module catches incoming errors! Handle errors as usual through asynchronous processing mechanisms such as callback functions. 🙂

But how exactly is the error passed inside the generator function?

var it = foo(); var res = it.next(); // { value:3, done:false } // instead of resuming normally with another `next(..) ` call, // let's throw a wrench (an error) into the gears: it.throw( "Oops!" ); // Error: Oops!Copy the code

In the code above, you’ll see another iterator method called throw(..). This method passes an error inside the generator as if it were thrown at the yield statement inside the generator. As you might expect, try… The catch module catches errors thrown by the throw method.

** Note: ** if you pass throw(..) Method throws an error inside the generator function without a try inside the function… The catch module catches an error that (like normal error bubbling mechanisms) will bubble from the generator function to the outside of the function (if the error is never handled, the error will bubble to the outermost layer as an uncaught error). The code is as follows:

function *foo() { }

var it = foo();
try {
    it.throw( "Oops!" );
}
catch (err) {
    console.log( "Error: " + err ); // Error: Oops!
}Copy the code

Obviously, the reverse error handling still works:

function *foo() {
    var x = yield 3;
    var y = x.toUpperCase(); // could be a TypeError error!
    yield y;
}

var it = foo();

it.next(); // { value:3, done:false }

try {
    it.next( 42 ); // `42` won't have `toUpperCase()`
}
catch (err) {
    console.log( err ); // TypeError (from `toUpperCase()` call)
}Copy the code

Proxy Generators function

Another thing you may want to do when using a generator function is to call another generator function inside the generator function. By this I do not mean executing a generator function inside a normal function, but actually delegating iteration control to another generator function. To do this, we use a variation of the yield keyword: yield *(” yield star “).

Such as:

function *foo() {
    yield 3;
    yield 4;
}

function *bar() {
    yield 1;
    yield 2;
    yield *foo(); // `yield *` delegates iteration control to `foo()`
    yield 5;
}

for (var v of bar()) {
    console.log( v );
}
// 1 2 3 4 5Copy the code

As mentioned in the first article (where I used function* foo() {} syntax instead of function* foo() {}), we still use yield* foo() instead of yield* foo(), Although many articles/documents prefer the latter syntax format. I think the previous syntax format expresses this syntactic meaning more accurately/clearly.

Let’s break down how the above code works. Yield 1 and yield 2 expressions pass values directly through for.. The of loop (implicitly) calls next() to be passed externally, as we already understand and expect.

During code execution, we encounter the yield * expression, and you will see that we give control to another generator function by executing foo(). Thus we basically produce/delegate an iterator to another generator function – this is perhaps the most accurate way to understand how a proxy generator function works.

Once the yield * expression (temporary) delegates control to the *foo() function in the *bar() function, now for.. The execution of the next() method in the of loop completely controls foo(), so yield 3 and yield 4 expressions pass their values through for.. The of loop returns to the outside.

When the *foo() run ends, control is handed back to the original generator function, and yield 5 is finally executed in the outer bar function.

For simplicity, in the above example we pass the value outside the generator function only using yield expressions, if we don’t use for.. Instead, we manually execute the iterator’s next() method to pass values inside the function, and these values will be passed to the generator via the yield * proxy as you would expect:

function *foo() {
    var z = yield 3;
    var w = yield 4;
    console.log( "z: " + z + ", w: " + w );
}

function *bar() {
    var x = yield 1;
    var y = yield 2;
    yield *foo(); // `yield*` delegates iteration control to `foo()`
    var v = yield 5;
    console.log( "x: " + x + ", y: " + y + ", v: " + v );
}

var it = bar();

it.next();      // { value:1, done:false }
it.next( "X" ); // { value:2, done:false }
it.next( "Y" ); // { value:3, done:false }
it.next( "Z" ); // { value:4, done:false }
it.next( "W" ); // { value:5, done:false }
// z: Z, w: W

it.next( "V" ); // { value:undefined, done:true }
// x: X, y: Y, v: VCopy the code

Although we have shown only one layer of nested generator functions in the code above, there is no reason why *foo() should not continue to proide other generator iterators, or even other generator functions, with yield * expressions, and so on.

The yield * expression provides another trick in that the yield * expression will return the function return value of the propped generator function.

function *foo() {
    yield 2;
    yield 3;
    return "foo"; // return value back to `yield*` expression
}

function *bar() {
    yield 1;
    var v = yield *foo();
    console.log( "v: " + v );
    yield 4;
}

var it = bar();

it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // "v: foo"   { value:4, done:false }
it.next(); // { value:undefined, done:true }Copy the code

As you can see, yield *foo() is proxying control of the iterator (calling the next() method) until its execution is complete, and the return value of foo() (in this case the string “foo”) will be used as the yield * expression value, which was assigned to the variable v in the previous example.

This is an interesting difference between yield and yield* expressions: In the yield expression, the return value of the expression is passed in a subsequent next() method call, but in the yield * expression, it gets the return value of the propped Generator function (because the next() method explicitly passes the value to the propped Generator function).

You can still error handle yield * proxies in both directions (as described above) :

function *foo() { try { yield 2; } catch (err) { console.log( "foo caught: " + err ); } yield; // pause // now, throw another error throw "Oops!" ; } function *bar() { yield 1; try { yield *foo(); } catch (err) { console.log( "bar caught: " + err ); } } var it = bar(); it.next(); // { value:1, done:false } it.next(); // { value:2, done:false } it.throw( "Uh oh!" ); // will be caught inside `foo()` // foo caught: Uh oh! it.next(); // { value:undefined, done:true } --> No error here! // bar caught: Oops!Copy the code

As you can see, throw(“Uh oh!” ) throws the error through the yield* proxy, and then * the try inside foo() function.. The catch module caught an error. Similarly, inside the *foo() function, throw “Oops!” Throws an error bubble into the *bar() function by another try.. The catch module catches, and if we don’t catch one of these errors, the error will continue to bubble up as you expect.

conclusion

Generators have synchronous execution semantics, which means you can use a try.. Catch error handling mechanism for error handling across yield statements. At the same time, generator iterators have a throw() method that throws an error at a pause in the generator function, which can still be passed through a try.. The catch module carries out the capture processing.

The yield * keyword allows you to delegate iteration control from the current generator function to another generator function. As a result, yield * plays a two-way role in information and error passing.

But so far, a fundamental question remains: how can generator functions help us handle asynchronous modes? In the previous two articles we have been discussing synchronous iteration patterns for generator functions.

The key to envisioning generator asynchrony is to start an asynchronous task by pausing the generator function and end the above asynchronous task by restarting the generator function (through execution of the iterator’s next() method). We can find various asynchronous control mechanisms for generator functions in the following articles. Look forward to the near future!