[Issue 846] You Don’t Know JS: Asynchronous Process Control

[Issue 846] You Don’t Know JS: Asynchronous Process Control


image


Preface

Asynchronous process control has been shared many times, so today’s morning article is to share this topic again. The translation and sharing of "You Don’t Understand JS Series" brought by the front-end morning reading class columnist @HetfieldJoe.


The text starts here~


If you have written a fair amount of JavaScript, this is no secret: asynchronous programming is a necessary skill. The main mechanism used to manage asynchrony was function callbacks.


However, ES6 adds a new feature: Promise, to help you solve the major flaw of using callbacks to manage asynchrony. In addition, we can revisit the generator (mentioned in the previous chapter) to see a pattern that combines the two. It is an important step forward for asynchronous flow control programming in JavaScript.


Promises


Let us identify some misunderstandings: Promises are not a substitute for callbacks. Promise provides a trusted intermediary mechanism-that is, between your calling code and the asynchronous code that will perform the task-to manage callbacks.


Another way to consider Promise is as an event listener, on which you can register to listen for an event that informs you when the task is complete. It is a time that is triggered only once, but can be regarded as an event anyway.


Promises can be chained together, and they can be a series of sequential, asynchronous steps. Together with high-level abstractions such as the all(..) method (in classic terms, called "door") and race(..) method (in classic terms, called "bolt"), the promise chain can provide a Mechanism of asynchronous flow control.


There is another way to conceptualize a Promise by thinking of it as a future value, a container of time-independent values. Regardless of whether the underlying value is the final value, this container can be reasoned the same. Observing the resolution of a Promise will extract the value when it is ready. In other words, a Promise is considered an asynchronous version of the return value of a synchronous function.


A Promise can only have two resolution results: completed or rejected, with an optional signal value. If a Promise is fulfilled, this final value is called a fulfilled value. If it is rejected, this final value is called the reason (that is, the "reason for rejection"). Promises can only be resolved (completed or rejected) once. Any other attempts to complete or reject will simply be ignored. Once a Promise is resolved, it becomes an immutable value (immutable).


Obviously, there are several different ways to think about what a Promise is. No angle is completely sufficient in its own right, but each angle provides an aspect of the whole. The main point of this is that they provide a major improvement to asynchrony using only callbacks, that is, they provide order, predictability, and credibility.


Create and use Promises


To construct a promise instance, you can use the Promise(..) constructor:

image.png


The Promise(..) constructor receives a single function (pr(..)), which is called immediately and receives two control functions in the form of parameter values, usually named resolve(..) and reject(.) .). They are used like this:

  • If you call reject(..), the promise will be rejected, and if any value is passed in reject(..), it will be set as the reason for rejection.

  • If you call resolve(..) without parameter values ​​or any non-promise value, the promise will be fulfilled.

  • If you call resolve(..) and pass in another promise, the promise will simply adopt-either immediately or finally-the state of the passed promise (either completed or rejected).


Here is how you usually use a promise to refactor a function call that depends on a callback. Suppose you start with an ajax (..) tool, which expects to call an error-first style callback:

image.png


You can convert it to:

image.png


Promise has a method then(..), which receives one or two callback functions. The first function (if it exists) is seen as the handler to be called when the promise is successfully fulfilled. The second function (if it exists) is seen as the handler to be called when the promise is explicitly rejected, or any error/exception is caught during the resolution process.


If one of these two parameter values ​​is omitted or is not a valid function-usually you would use null instead-then a default equivalent for a placeholder will be used. The default success callback will pass its completion value, and the default error callback will propagate its rejection reason.


The abbreviation for calling then(null, handleRejection) is catch(handleRejection).


Both then(..) and catch(..) both automatically construct and return another promise instance, which is linked to the original promise and receives the resolution result of the original promise - (actually called) completion or Any value returned by the processor is rejected. Consider the following code:

image.png


In this code snippet, we either return an immediate value from fulfilled(..) or reject(..) return an immediate value, and then in the next event cycle this immediate value is replaced by the second then(..) fulfilled(..) received. If we return a new promise, then this new promise will be included and adopted as the resolution result:

image


It should be noted that an exception (or promise rejection) in the first fulfilled(..) will not cause the first rejected(..) to be called, because this processing will only answer the first original promise Analysis. Instead, the second promise for the second then(..) call will receive this rejection.


In the above code snippet, we did not monitor this rejection, which means it will be kept quietly for future observations. If you never observe it by calling then(..) or catch(..), then it will become unprocessed. The developer console of some browsers may detect these unhandled rejections and report them, but this is not guaranteed; you should always observe promise rejections.


Note: This is just a brief overview of Promise theory and behavior. For a more in-depth exploration, see Chapter 3 of Asynchrony and Performance in this series.


Thenables


Promise is a pure instance of Promise(..) constructor. However, there is also a promise-like object called thenable, which can usually cooperate with the Promise mechanism.


Any object (or function) with a then(..) function is considered a thenable. Any place where the Promise mechanism can accept and adopt a pure promise state can handle a thenable.


Thenable is basically a generalized label that identifies any promise value created by any system other than the Promise(..) constructor. From this perspective, a thenable is not as credible as a pure Promise. For example, consider this abnormally behaving thenable:

image


If you receive this thenable and use th.then(..) to link it, you may be surprised to find that your completion handler is called repeatedly, while ordinary Promises should only be resolved once.


Generally speaking, if you receive something that claims to be promise or thenable from some other system, you should not believe it blindly. In the next section, we will see an ES6 Promise tool that can help solve the trust problem.


But in order to further understand the dangers of this problem, let us consider that any object in any piece of code, as long as it has been defined as having a method called then(..), will potentially be mistaken for a thenable -Of course, if used with Promises-whether this thing is intentionally related to Promise-style asynchronous coding or not.


Before ES6, there was never any special preservation measures for the method called then(..). As you can imagine, there were at least a few cases before the Promise appeared on the radar screen. It has been selected as The name of the method. The most likely use of thenable is that the asynchronous library that uses then(..) is not strictly Promise-compatible - there are several on the market.


The burden will be borne by you: to prevent values ​​that would be mistaken for a thenable from being directly used in the Promise mechanism.


Promise API


PromiseAPI also provides some static methods for handling Promises.


Promise.resolve(..) creates a promise that is resolved to the value passed in. Let's compare how it works with a more manual method:

image


p1 and p2 will have exactly the same behavior. Using a promise for resolution is the same:

image


Tip: Promise.resolve(..) is the solution to the thenable trust problem proposed in the previous section. Any value that you are not sure about a trusted promise-it may even be an immediate value-can be normalized by passing in Promise.resolve(..). If this value is already a recognizable promise or thenable, its state/resolution result will simply be adopted to isolate the wrong behavior from you. If instead it is an immediate value, then it will be "wrapped" into a pure promise to normalize its behavior as asynchronous.


Promise.reject(..) creates a promise that is rejected immediately, just like its Promise(..) constructor equivalent:

image


Although resolve(..) and Promise.resolve(..) can receive a promise and adopt its state/resolution result, reject(..) and Promise.reject(..) will not distinguish what they receive value. So, if you use a promise or thenable for rejection, the promise/thenable itself will be set as the reason for rejection, not its underlying value.


Promise.all([ .. ]) accepts an array of one or more values ​​(for example, immediate value, promise, thenable). It returns a promise that will be fulfilled when all the values ​​are fulfilled, or rejected immediately when the first rejected value among these values ​​appears.


Use these values/promises:

image


Let's consider how Promise.all([ .. ]) works using a combination of these values:

image


Promise.all([ .. ]) waits for all values ​​to complete (or the first rejection), while Promise.race([ .. ]) only waits for the first completion or rejection. Consider the following code:

image


Warning: Although Promise.all([]) will complete immediately (without any value), Promise.race([]) will be suspended forever. This is a strange inconsistency, and I suggest that you should never call these methods with an empty array.


Generators + Promises


It is possible to express a series of promises in a chain to represent the asynchronous flow control of your program. Consider the following code:

image


But there are better options for expressing asynchronous flow control, and the code style may be more ideal than a long promise chain. We can use the generator learned in Chapter 3 to express our asynchronous flow control.


To identify an important pattern: a generator can yield a promise, and then this promise can use its fulfillment value to advance the generator.


Consider the previous code segment and use generator to express:

image


On the surface, this code snippet is more verbose than the previous promise chain equivalent. But it provides more attractive-and importantly, easier to understand and read-seemingly synchronous code style ("return" value = assignment operation, etc.), for try..catch error handling This is especially true when it can be used across hidden asynchronous boundaries.


Why should we use Promise with generator? It is certainly possible to code asynchronous generators without Promise.


Promise is a trusted system that reverses normal callbacks and control inversions that occur in thunks (see asynchrony and performance in this series). Therefore, combining the credibility of Promise and the synchronization of the code in the generator effectively solves the main defect of callback. In addition, tools like Promise.all([ .. ]) are a very nice and clean way to express concurrency in a yield step of a generator.


So how does this magic work? We need a runner that can run our generator, receive a promise that is yielded and connect to it, so that it either pushes the generator with successful completion, or throws an exception to the generator with the reason of rejection.


Many tools/libraries with asynchronous capabilities have such "runners"; for example, Q.spawn (..) and the runner (..) plugin in my asynquence. Here is a standalone runner to show how this processing works:


image.png

Note: For a more annotated version of this tool, see Asynchrony and Performance in this series. In addition, the runtime tools provided by various asynchronous libraries are usually more powerful than what we show here. For example, asynquence's runner(..) can handle promises, sequences, thuns, and (non-promise) indirect values ​​that are yielded, giving you ultimate flexibility.


So now running *main() in the earlier code snippet is as easy as this:

image.png


In essence, anywhere in your program that has more than two asynchronous steps of flow control logic, you can and should use a promise-yielding generator driven by a running tool to express flow control in a synchronous style. Doing so will produce code that is easier to understand and maintain.


This "give up a promise to advance the generator" model will be so common and so powerful that the next version of JavaScript after ES6 is almost certain to introduce a new type of function, which can be automated without running tools地Execute. We will explain async functions (as they are expected to be called) in Chapter 8.


review


With the increasing maturity and growth of JavaScript in its widespread adoption, asynchronous programming has increasingly become the center of attention. Callbacks are not completely sufficient for these asynchronous tasks, and they collapse in the face of more sophisticated requirements.


Fortunately, ES6 added Promise to solve one of the main flaws of callbacks: lack of credibility in predictable behavior. Promise represents the future completion value of a potential asynchronous task, and normalizes behavior across the boundary between synchronization and asynchronous.


However, the combination of Promise and generator fully reveals the benefits of doing so: rearranging our asynchronous flow control code, weakening and abstracting the ugly callback paste (also called "hell").


At present, we can manage these interactions with the help of various asynchronous library runners, but JavaScript will eventually use a special independent syntax to support this interaction mode!


Guess you like

Origin blog.51cto.com/15080028/2595023