Original text: davidwalsh. Name/async – gener…

Now that you know the basics and features of ES6 generators, it’s time to improve the code in the real world.

The main capability of generators is to provide a single-threaded code style similar to synchronous patterns, while allowing asynchronous operations to be hidden as implementation details. This allows us to view our code flow in a very natural way, without having to deal with asynchronous syntax and related issues.

In other words, we get a nice separation of functionality and concerns by combining the consumption of data (generator logic) with the implementation details of asynchronously retrieving data (next(..)). ) isolated from.

The result? We get all the power of asynchronous code and the ease of reading and maintaining synchronous (seemingly) code.

So how do we do that?

The simplest asynchrony

In short, generators do not require anything extra for a program to gain the ability to control asynchrony.

For example, suppose you already have the following code.

Function makeAjaxCall(url,cb) {makeAjaxCall("http://some.url.1", function(result1){ var data = JSON.parse( result1 ); makeAjaxCall( "http://some.url.2/? id=" + data.id, function(result2){ var resp = JSON.parse( result2 ); console.log( "The value you asked for: " + resp.value ); }); });Copy the code

To implement the above program with a generator (without adding any modifier), do this:

Function Request (url) {// This is where we make the asynchronous process, separate from the main generator code, // 'it.next(..) 'is a call to makeAjaxCall(URL, function(response){it.next(response); }); // Note: nothing is returned here! } function *main() { var result1 = yield request( "http://some.url.1" ); var data = JSON.parse( result1 ); var result2 = yield request( "http://some.url.2? id=" + data.id ); var resp = JSON.parse( result2 ); console.log( "The value you asked for: " + resp.value ); } var it = main(); it.next(); // Start executing all the codeCopy the code

Here’s how the code works.

Help function request(..) Just make the old makeAjaxCall(..) The function is wrapped to ensure that its callback can call next(..) of the generator’s iterator. Methods.

For the request (“..” ), notice that it does not return any value (in other words, the return value is undefined). Here, yield undefined is no big deal, but the rest of the article is more important.

We then call yield.. (undefined), nothing is done here, but the generator is paused here. The generator waits until it calls it.next(..) Make it continue executing, which happens when the Ajax call completes.

But the yield.. What happens to the result of this expression? We assign the result of the expression to traversal result1. How does the variable get this value in the first Ajax call?

This is due to calling it.next(..) in the Ajax callback function. When, the Ajax response data is passed to the generator, that is, the value is passed to the place where the generator pauses, i.e., result1 = yield.. Statement!

It’s cool. It’s powerful. In essence, result1 = yield request(..) The data is requested, but the process is (almost) completely hidden — at least not a concern here — and the process of implementing this step is asynchronous. The asynchronous process is implemented with yield’s pause, and the ability to continue the generator is assigned to other functions, allowing the code to implement a synchronous (seemingly) data request.

Second result2 = yield result(..) The statement is exactly the same: it implicitly pauses and continues, and gets the data we need, but we don’t have to worry about the asynchrony details when coding.

Of course, there’s yield, which is a subtle hint that some magic might happen. But yield is a small syntactic signal, or overhead, compared to nested callback functions (or the API overhead associated with the Promise chain).

Notice how I said “could happen.” This is actually a very powerful thing in and of itself. The above program does make asynchronous Ajax calls, but what if it doesn’t? What if we later modified the program to use the results of the previous (or pre-fetched) Ajax response? Or does the application’s URL router make it possible, in some cases, to respond immediately to Ajax requests without having to fetch data from the server?

We can put request(..) The implementation of

var cache = {}; Function request(url) {if (cache[url]) {setTimeout(function(){it.next(cache[url]); }, 0); } else { makeAjaxCall( url, function(resp){ cache[url] = resp; it.next( resp ); }); }}Copy the code

Note: There is a subtle trick here, which is to use setTimeout(.. 0) to create a delay. If we immediately call it.next(..) , an error is generated because (and this is the tip part) the generator has not technically entered a pause state. Function call request(..) It needs to be executed first, and then yield pauses. So, we can’t immediately request(..) Call it. Next (..) Because the generator is running at this point (yield has not yet been executed). But we can call it.next(..) “later”. Execute immediately after the current thread of execution completes. This is called setTimeout(.. 0) implement “hack”. A better answer to this question is provided below.

Now, the main generator code still looks like this:

var result1 = yield request( "http://some.url.1" ); var data = JSON.parse( result1 ); .Copy the code

See? ? The generator logic (that is, flow control) does not change from the version without cache control.

* The code in main() is still requesting data, pausing until the data is returned and then continuing. In the current scenario, the “pause” can be very long (producing a real server request, about 300-800ms) or very quick (setTimeout(.. 0) the resulting delay). But process control doesn’t care.

This is the power of abstracting the implementation details of an asynchronous process.

Better asynchronous

The above approach is sufficient for small asynchronous generators. But it can quickly hit a bottleneck, so we need to use a more powerful asynchronous mechanism to handle more complex cases. What mechanism? Promise.

If you’re not familiar with ES6 Promise, I’ve written a few introductory articles that you can read, and we’ll pick up when you get back.

The previous Ajax code example has the same problem with inversion of control as the original nested callback example. Here are some of the current issues:

  1. There is no clear pattern for exception handling. As mentioned in the previous article, we can detect errors in Ajax calls by it.throw(..). Pass it to the generator and then pass the try.. Catch to deal with it. But this is just more manual work, and the code may not be reusable in our program.

  2. If makeAjaxCall (..) The tool is not under our control, callbacks are called multiple times, successful and failed callbacks are called simultaneously, and so on, and the generator is a mess (uncaught exceptions, unexpected values, and so on). Dealing with and avoiding such problems means repetitive work and is probably not portable.

  3. We often need to process more than one task “in parallel” (for example, two Simultaneous Ajax calls). Because a generator’s yield statement is a single pause point, two or more cannot be executed at the same time — they can only be executed sequentially, one at a time. So, it’s not clear how you can handle multiple tasks with one yield without adding more code.

As you can see, all of these problems can be solved, but who wants to do the same thing every time? We need a powerful pattern that provides a trusted, reusable solution for generator-based asynchronous programming.

What is the pattern? Throw promises to continue the generator’s execution.

We used yield request(..) above. And the request (..) Is yield undefined efficient enough without returning any value?

Let’s modify the code a little bit. Will request (..) Make it promise based so that you can return a promise, so that yield actually returns a promise (instead of undefined).

Function request(url) {return a promise! return new Promise( function(resolve,reject){ makeAjaxCall( url, resolve ); }); }Copy the code

request(..) Now that you’ve constructed a promise (and made it Resolved when the Ajax call completes), return the promise so that it can be yield thrown. And then what?

We need a tool to control the iterators of the generator, which will collect the promises made by executing the generator (via next(..)) ). I will now call this tool runGenerator(..) :

Function runGenerator(g) {var it = g(), ret; // Iterate (function iterate(val){ret = it. Next (val); if (! Ret.done) {// The poor version of "Is this a Promise?" Check if ("then" in ret.value) {// wait for promise ret.value. Then (iterate); Else {// Avoid synchronizing setTimeout(function(){iterate(ret.value); }, 0); }}}) (); }Copy the code

The main points to note are:

  1. Initialize the generator automatically (create the corresponding IT iterator) and then execute it asynchronously until done:true.

  2. Check to see if a promise is returned (i.e., each it.next(..)) Return value of the call), if so, then(..) is registered with the Promise. To wait for the promise to complete.

  3. If there is an immediate (non-PORmise) value returned, it is passed back to the generator so that the generator can continue execution.

Now, how do you use it?

runGenerator( function *main(){
    var result1 = yield request( "http://some.url.1" );
    var data = JSON.parse( result1 );

    var result2 = yield request( "http://some.url.2?id=" + data.id );
    var resp = JSON.parse( result2 );
    console.log( "The value you asked for: " + resp.value );
} );Copy the code

And so on… Isn’t that exactly the same generator code as before? Yes. Once again, we see the power of generators. Now we create promsie, yield it externally, wait for it to finish and continue executing the generator — all of these are “hidden” implementation details! This is not really hidden, but separated from the consuming code (the flow control of the generator).

By waiting for the promise returned by yield and returning the data to it.next(..) when it completes. , result1 = yield request(..) The statement gets the value as before.

Now, however, we use promises to manage the asynchronous part of the generator code, addressing the inversion and trust issues as opposed to just using callback functions. All of the above solutions are available “for free” through Generator + Promise:

  1. We now have built-in exception handling that is easy to use. Despite the runGenerator(..) above Is not shown, but listens for errors from promise and passes them through it.throw(..) Passing is not complicated — then try.. Catch catches and handles these errors within the generator code.

  2. We gained the control and trust promise provided. No need to worry, no need to fuss.

  3. Promise also provides a number of powerful abstraction mechanisms to address the complexity of multiple “parallel” tasks, and so on.

    For example, yield promise.all ([..] ) can be used to “parallel” a set of promises, returned as a single promise (handed over to the generator) that will wait until the additional sub-Promise tasks are complete. What is returned from the yield expression (when the promise completes) is the corresponding set of promSIE response data, in the order requested (regardless of the order in which it was actually completed).

First, let’s look at exception handling:

// Assume: 'makeAjaxCall(..) 'now receives an' exception-first style 'callback function (omitted) // Assume:' runGenerator(..) Function request(url) {return new Promise(function(resolve,reject){makeAjaxCall(url,  function(err,text){ if (err) reject( err ); else resolve( text ); }); }); } runGenerator( function *main(){ try { var result1 = yield request( "http://some.url.1" ); } catch (err) { console.log( "Error: " + err ); return; } var data = JSON.parse( result1 ); try { var result2 = yield request( "http://some.url.2? id=" + data.id ); } catch (err) { console.log( "Error: " + err ); return; } var resp = JSON.parse( result2 ); console.log( "The value you asked for: " + resp.value ); });Copy the code

If a promise rejection (or error/exception of any kind) occurs during the process of getting the URL, the promise rejection is mapped to a generator error (by — not showing — runGenerator(..)). In it. Throw (..) ) and then be tried.. Catch statement.

Next, let’s look at an example of using PROMsie to handle more complex asynchrony:

function request(url) { return new Promise( function(resolve,reject){ makeAjaxCall( url, resolve ); }) // Perform further processing based on the returned text. Then (function(text){// Get a (redirect) URL. if (/^https? :\/\/.+/.test(text)) {return request(text); } // Otherwise, assume that the text is the expected data else {return text; }}); } runGenerator( function *main(){ var search_terms = yield Promise.all( [ request( "http://some.url.1" ), request( "http://some.url.2" ), request( "http://some.url.3" ) ] ); var search_results = yield request( "http://some.url.4? search=" + search_terms.join( "+" ) ); var resp = JSON.parse( search_results ); console.log( "Search results: " + resp.value ); });Copy the code

Promise.all([ .. ] ) constructs a promise that waits for three internal promises to complete, yielding to runGenerator(..). To listen to continue the generator’s promise. An internal promise can receive a response that looks like a redirected URL, and then chain another internal promise to the new address. For more on the Promise chain, read this article.

All of the promise capabilities that can be used in asynchronous mode can be achieved through yield promises (or promises of promises…) in generators. To get it in synchronized code. This is the best of both worlds.

runGenerator(..)Tool library:

Above we define our own runGenerator(..) To get this combination of generator + promise. We even left out (for brevity) the full implementation of the tool and the various details related to exception handling.

However, you don’t want to write runGenerator(..) yourself. Isn’t it?

I guess so.

Various Promise or asynchronous libraries already provide similar tools. I won’t go through them all here, but you can look at Q.s Pawn (..) , co (..) Libraries, and so on.

Next I’ll briefly introduce my own library: Asynquence Runner (..) Plugins, because I think it offers some unique capabilities. If you want to learn more, check out this article.

First, Asynquence provides tools for automatically handling the above “error-first style” :

Function request(url) {return ASQ(function(done){// Pass an error-first style callback makeAjaxCall(url, done.errfcb); }); }Copy the code

That’s better, isn’t it?

Next, asynquence runner(..) The plugin consumes a generator in an asynquence queue (an asynchronous sequence of steps), which can pass data from the previous step. The generator can pass data down to the next step, and all errors are automatically collected:

// Call 'getSomeValues()' to create a sequence/promise, Runner (function*(token){// token.messages will be filled with the data from the previous step value1 = token.messages[0]; var value2 = token.messages[1]; var value3 = token.messages[2]; // Make three Ajax requests in parallel and wait for all requests to complete // Note: 'ASQ().all(..) ` similar ` Promise. All (..) ` var msgs = yield ASQ().all( request( "http://some.url.1? v=" + value1 ), request( "http://some.url.2? v=" + value2 ), request( "http://some.url.3? v=" + value3 ) ); Yield (MSGS [0] + MSGS [1] + MSGS [2]); }) // use the last result data of the previous generator to generate a new Ajax request. seq(function(MSG){return request("http://some.url.4? msg=" + msg ); }) // Now all is done! .val( function(result){ console.log( result ); // Success, all completed! }) // Or, there are errors! .or( function(err) { console.log( "Error: " + err ); });Copy the code

Asynquence runner (..) Start execution of the generator by receiving (optional) data from the previous step in the queue, which can then be used within the generator via the token.message array.

Then, the runGenerator(..) shown above So, runner (..) Listen for promises returned by yield or asynchronous queues (ASQ().all(..) ), and then wait for it to complete to continue executing the generator.

When the generator execution is complete, the final value is used by yield for the next step in the queue.

Also, if errors occur, even within the generator, bubbles to or(..). The error handler registered here.

Asynquence tries to mix promise and generator and keep it as simple as possible. You are free to use the generator flow with the Promise-based queue flow.

ES7 async

One proposal that is likely to be adopted in ES7 is to introduce another new function type: async Function, which acts like a generator wrapped automatically in a similar runGenerator(..). (or asyncquence runner(..) ). In this way, you can return a promise, and async Function will automatically bind it and continue its execution after completion (without even an iterator!). .

It will probably look something like this:

async function main() { var result1 = await request( "http://some.url.1" ); var data = JSON.parse( result1 ); var result2 = await request( "http://some.url.2? id=" + data.id ); var resp = JSON.parse( result2 ); console.log( "The value you asked for: " + resp.value ); } main();Copy the code

As you can see, async function can be called directly (main()) without wrapping. Internally, instead of using yield, we use await (another new keyword) to tell the async function to wait for the promise to complete before continuing.

Basically, most of the functionality of tool-wrapped generators is provided, but is directly supported by native syntax.

Cool, isn’t it? ?

Also, libraries like Asynquence provide running utility functions that make it easy to use asynchronous generators!

conclusion

Simply combine generator + yield promsie to get the best of both worlds, which is powerful functionality and elegant synchronous (and seemingly asynchronous) process control. Using simple wrapping tools (which many libraries already provide), we can automatically execute generators and support robust and synchronous (seemingly) exception handling!

In the ES7+ space, we will likely see Async Function support these features (at least in the basic case) without the need for additional libraries!

The future of asynchronous JavaScript programming is bright, and it’s only getting brighter! I’m gonna have to wear sunglasses.

But it didn’t end there. There is one final area to explore:

What if two or more generators were combined so that they could execute separately “in parallel” and pass data to each other as they execute? That would be a very powerful power, wouldn’t it! ? ! This mode is called “CSP” (Communicating sequential processes). In the next article we will discover and unlock the CSP functionality. Don’t blink!

as

I am not familiar with ES6 native Promise and the asynquence library mentioned in this article. Please correct any mistakes or omissions.

In addition, in the process of translation, the text gradually only pursues “meaning”, which may not fully correspond with the original text. Students who mind this and can read English properly recommend reading the original text.