📜 ⬆️ ⬇️

We refuse callbacks: Generators in ECMAScript 6

I constantly hear people whining about asynchronous callbacks in JavaScript . Keeping in mind the order of execution in this language is a bit difficult (this is the case that is called “ Callback Hell ” or “ The Pyramid of Doom ”), if you have dealt with synchronous programming before. My usual answer was “you have to deal with it somehow.” After all, do we expect all programming languages ​​to look and feel the same? Of course not.

Everything was changed by a recent review of the draft ECMAScript 6 , which describes the generators - the possibility of a language that will completely change our way of writing both server-side and client-side JavaScript . With the help of generators, we can turn nested callbacks into a similar synchronous code without blocking our only event loop .
For example, this code:
  setTimeout(function(){ _get("/something.ajax?greeting", function(err, greeting) { if (err) { console.log(err); throw err; } _get("/else.ajax?who&greeting="+greeting, function(err, who) { if (err) { console.log(err); throw err; } console.log(greeting+" "+who); }); }); }, 1000); 

can be written like this:
  sync(function* (resume) { try (e) { yield setTimeout(resume, 1000); var greeting = yield _get('/something.ajax?greeting', resume) var who = yield _get('/else.ajax?who&greeting=' + greeting, resume) console.log(greeting + ' ' + who) } catch (e) { console.log(e); throw e; } }); 

Interesting, isn't it? Centralized exception handling and clear order of execution.


Uh, ECMAScript 6?


The examples in this article will work in Chrome Canary 33.0.1716.0 . Examples, except for those with XHR , should work in Node.js with the --harmony flag (from version 0.11, approx. Transl. ). The generator implementation proposed in JavaScript 1.7+ does not follow ECMAScript 6 draft — so you will have to make some changes to make the examples work in Firefox . If you want to run these examples in Canary , you can run them in the same way as here.
')

ES6 Generators


In order to understand what is happening in the examples above, we need to talk about what are ES6 generators and what they allow you to do.

According to the ECMAScript 6 draft, generators are “first-class coroutines that are objects that encapsulate pending execution contexts.” Simply put, generators are functions that can stop their execution (using the yield keyword) and continue their execution from the same place after calling their next method. JavaScript still performs only one task at the same time, but it is now able to pause execution in the middle of the body of a generator function and switch the context to something else. Generators do not allow parallel execution of code and they do not know how to handle threads.

Modest iterator


Now that we've figured out a bit, let's look at the code. We will write a small iterator to demonstrate the stop / continue syntax.
  function* fibonacci() { var a = 0, b = 1, c = 0; while (true) { yield a; c = a; a = b; b = c + b; } } function run() { var seq = fibonacci(); console.log(seq.next().value); // 0 console.log(seq.next().value); // 1 console.log(seq.next().value); // 1 console.log(seq.next().value); // 2 console.log(seq.next().value); // 3 console.log(seq.next().value); // 5 } run(); 

What's going on here:
  1. The run function initializes the Fibonacci number generator (it is described by the special syntax funtion* ). Unlike the usual function, this call does not start the execution of its body, but returns a new object - the generator.
  2. When the run function calls the generator method next (a synchronous operation), the code is executed until it reaches the yield .
  3. Running the yield stops the generator and returns the result to the outside. The operations following yield have not been performed at this point. The value (operand a for yield ) will be available outside through the value property of the result of the execution.
    The next time the generator calls the next method, the execution of the code continues from where it stopped at the previous yield .


You are probably wondering if the generator will ever come out of a while . No, it will be executed inside the loop until someone calls its next method.

We follow the execution of the code


As shown in the previous example, the code located in the body of the generator after yield will not be executed until the generator continues. You can also pass an argument to the generator, which will be substituted instead of the yield on which the previous execution of the generator was interrupted.
  function* powGenerator() { var result = Math.pow(yield "a", yield "b"); return result; } var g = powGenerator(); console.log(g.next().value); // "a", from the first yield console.log(g.next(10).value); // "b", from the second console.log(g.next(2).value); // 100, the result 

The first execution of the generator returns the value "a" as the value property of the execution result. Then we continue execution by passing a value of 10 to the generator. Use the substitution to demonstrate what happens:
  function* powGenerator() { var result = Math.pow(----10----, yield "b"); return result; } 

Then the generator reaches the second yield and again suspends its execution. The value "b" will be available in the returned object. Finally, we continue execution again, passing in argument 2 . Substitution again:
  function* powGenerator() { var result = Math.pow(----10----, ----2----); return result; } 

After this, the pow method is called, and the generator returns the value stored in the result variable.

Unreal synchrony: blocking Ajax


The iterator, issuing the Fibonacci sequence, and the mathematical functions with a lot of entry points are interesting, but I promised to show you a way to get rid of callbacks in your JavaScript code. As it turns out, we can take some ideas from previous examples.

Before we look at the following example, pay attention to the sync function. It creates a generator, passing it the resume function and calls the next method on it to start its execution. When the generator needs an asynchronous call, it uses resume as a callback and performs a yield . When an asynchronous call performs resume , it calls the next method, continuing the execution of the generator and passing the result of the asynchronous call to it.

Back to code:
  // ************** // framework code function sync(gen) { var iterable, resume; resume = function(err, retVal) { if (err) iterable.raise(err); iterable.next(retVal); // resume! }; iterable = gen(resume); iterable.next(); } function _get(url, callback) { var x = new XMLHttpRequest(); x.onreadystatechange = function() { if (x.readyState == 4) { callback(null, x.responseText); } }; x.open("GET", url); x.send(); } // **************** // application code sync(function* (resume) { log('foo'); var resp = yield _get("blix.txt", resume); // suspend! log(resp); }); log('bar'); // not part of our generator function's body 

Can you guess what you see in the console? The correct answer is “foo”, “bar” and “what’s in blix.txt”. Placing the code inside the generator, we make it look like a normal synchronous code. We do not block the event loop ; we stop the generator and continue to execute the code located further after the next call. The future callback, which will be called on another tick, will continue our generator, passing it the desired value.

Centralized error handling


Centralized error handling within multiple asynchronous callbacks is a pain. Here is an example:
  try { firstAsync(function(err, a) { if (err) { console.log(err); throw err; } secondAsync(function(err, b) { if (err) { console.log(err); throw err; } thirdAsync(function(err, c) { if (err) { console.log(err); throw err; } callback(a, b, c); }); }); }); } catch (e) { console.log(e); } 

The catch will never be executed due to the fact that the execution of the callback is part of a completely different callstack, in another tick of the event loop . Exception handling must be located inside the callback function itself. You can implement a higher order function to get rid of some duplicate error checks and remove some attachments using a library like async . If you follow the Node.js convention about the error as the first argument, you can write a common handler that will return all errors back to the generator:
  function sync(gen) { var iterable, resume; resume = function(err, retVal) { if (err) iterable.raise(err); // raise! iterable.next(retVal); }; iterable = gen(resume); iterable.next(); } sync(function* (resume) { try { var x = firstAsync(resume); var y = secondAsync(resume); var z = thirdAsync(resume); // … do something with your data } catch (e) { console.log(e); // will catch errors from any of the three calls } }); 

Now the exceptions that occur within any of the three functions will be processed by a single catch . And the exception that occurred in any of the three functions will not allow subsequent functions to be executed. Very good.

Simultaneous operations.


The fact that the generator code is executed from top to bottom does not mean that you cannot work with several asynchronous operations simultaneously. genny like genny and gen-run give this API: they simply perform a number of asynchronous operations before continuing the execution of the generator. Example using genny :
  genny.run(function* (resume) { _get("test1.txt", resume()); _get("test2.txt", resume()); var res1 = yield resume, res2 = yield resume; // step 1 var res3 = yield _get("test3.txt", resume()); // step 2 console.log(res1 + res2); }); 

Total


Asynchronous de facto callbacks have been the main pattern in JavaScript for a long time. But now with the generators in the browser ( Firefox with JavaScript 1.7 and Chrome Canary few months ago) everything is changing. New execution control constructs make it possible to use a completely new programming style, one that can compete with the traditional nested callback style. It remains to wait for the ECMAScript 6 standard to be implemented in tomorrow's JavaScript engines.

Source: https://habr.com/ru/post/210330/


All Articles