preface

One of the things we’ve heard since we first started learning JavaScript is that JS is single-threaded, asynchronous by nature, and suitable for IO intensive, not CPU intensive. However, most JavaScript developers have never really thought about how and why asynchrony occurs in their programs, nor have they explored other ways to handle asynchrony. So far, many people have insisted that the callback function is sufficient.

But as JavaScript continues to grow in size and complexity to meet the ever-increasing demands it faces to run on browsers, servers, and even embedded devices, and the pain of using callback functions to manage asynchrony, In this article, I want to summarize the existing asynchronous JavaScript processing methods, and try to explain why these technologies appear, so that we have a broader understanding of asynchronous JavaScript programming, so that knowledge becomes more systematic.

This article will also be synchronized to my personal website.

The body of the

Step1 – callback function

The callback function is familiar to everyone, starting with the simplest timer:

setTimeout(function () {
    console.log('Time out');
}, 1000);Copy the code

The anonymous function inside the timer is a callback function, and since functions are first-class citizens in JS, they can be passed as arguments just like any other variable. In this case, using callback functions to handle asynchrony is a good way to write it, so why use other methods?

Let’s look at a requirement like this:

The above is the login sequence diagram of wechat applet. Our requirements are similar but somewhat different. To obtain a piece of business data, the whole process is divided into 3 steps:

  1. Call the key interface to get the key
  2. Invoke the login interface with the key to obtain the token and userId
  3. Invoke the service interface with the token and userId to obtain data

The above steps may be somewhat different from the actual business, but they can be used to explain the problem, please understand.

Let’s write some code to implement the above requirements:

let key, token, userId; $.ajax({ type: 'get', url: 'http://localhost:3000/apiKey', success: function (data) { key = data; $.ajax({ type: 'get', url: 'http://localhost:3000/getToken', data: { key: key }, success: function (data) { token = data.token; userId = data.userId; $.ajax({ type: 'get', url: 'http://localhost:3000/getData', data: { token: token, userId: userId }, success: Function (data) {console.log(' data: ', data); }, error: function (err) { console.log(err); }}); }, error: function (err) { console.log(err); }}); }, error: function (err) { console.log(err); }});Copy the code

As you can see, the entire code is full of callback nesting, and the code expands not only vertically but horizontally as well. I’m sure debugging can be difficult for anyone, and we have to jump from one function to the next and the next, jumping through the code to see the flow, with the final result hidden in the middle of the whole code. Real JavaScript code is likely to be much messier, making this tracing exponentially harder. This is what we call Callback Hell.

Why does this happen?

If one business depends on data from the upper business, and the upper business depends on data from the lower level, and we use callback to handle asynchrony, then callback hell happens.

The way the brain plans things is linear, blocking, single-threaded semantics, but the way callbacks express asynchronous flows is non-linear and out-of-order, making such code difficult and bug-prone to derive correctly.

Here we introduce callback functions to solve the first problem of asynchrony: callback hell.

Is there anything else wrong with callbacks? Let’s think a little more deeply about the concept of a pullback:

// A
$.ajax({
    ...
    success: function (...) {
        // C
    }
});
// BCopy the code

A and B occur now, under the direct control of the JavaScript main program, while C is deferred to the future and under the control of A third party, in this case the function $.ajax(…). . Fundamentally, this transfer of control usually doesn’t cause a lot of problems for the program.

However, don’t let this small probability fool you into thinking that this control switch isn’t a big deal. In fact, this is the most serious (and subtle) problem with callback-driven design. It centers on the idea that sometimes Ajax (…) That is, the third party you deliver the callback function to is not the code you wrote or under your direct control, it is a tool provided by a third party.

This condition is called inversion of control, where you hand over control of the execution of a part of your program to a third party and have an unexpressed contract directly between your code and the third-party tool.

Since your callback is executed by a third party out of your control, the following problems can occur, which usually do not occur:

  1. Calling the callback too early
  2. Calling the callback too late
  3. Too many callbacks or too few callbacks
  4. Failed to pass the required parameters to your callback function
  5. Swallow possible errors or exceptions
  6. .

This control reverse transfer results in a complete breakdown of the trust chain, and if you haven’t taken action to address the trust issues caused by these reversals of control, then your code already has hidden bugs, even though most of us don’t.

Here, we introduce the second problem with callbacks handling asynchrony: inversion of control.

To sum up, there are two problems with callback functions handling asynchronous processes:

1. Lack of sequence: Debugging difficulties caused by callback hell, inconsistent with the way the brain thinks 2. Lack of trust Capriciousness: A series of trust problems caused by inversion of control

So how to solve these two problems, the pioneers began to explore the road……

Step2 – Promise

To cut to the chase, Promise addresses the second problem with callbacks handling asynchrony: inversion of control.

As far as promises are concerned, this is the PromiseA+ specification, ES6 Promise, jQuery Promise. Different libraries have different implementations, but they all follow the same set of specifications, so promises don’t refer to any particular implementation. It’s a specification, a set of mechanisms for dealing with JavaScript asynchrony.

Let’s refactor the above nested multi-layer callback with Promise:

let getKeyPromise = function () { return new Promsie(function (resolve, reject) { $.ajax({ type: 'get', url: 'http://localhost:3000/apiKey', success: function (data) { let key = data; resolve(key); }, error: function (err) { reject(err); }}); }); }; let getTokenPromise = function (key) { return new Promsie(function (resolve, reject) { $.ajax({ type: 'get', url: 'http://localhost:3000/getToken', data: { key: key }, success: function (data) { resolve(data); }, error: function (err) { reject(err); }}); }); }; let getDataPromise = function (data) { let token = data.token; let userId = data.userId; return new Promsie(function (resolve, reject) { $.ajax({ type: 'get', url: 'http://localhost:3000/getData', data: { token: token, userId: userId }, success: function (data) { resolve(data); }, error: function (err) { reject(err); }}); }); }; getKeyPromise() .then(function (key) { return getTokenPromise(key); }) .then(function (data) { return getDataPromise(data); }). Then (function (data) {console.log(' data: ', data); }) .catch(function (err) { console.log(err); });Copy the code

As you can see, Promise actually improves the way callback functions are written to some extent, most notably by eliminating horizontal extensions through multiple THEN (…), no matter how many business dependencies there are. To get the data so that the code only extends vertically; Another point is that the logic is more obvious. By extracting the asynchronous business into a single function, the entire process can be seen to be executed step by step, the dependency hierarchy is clear, and the data required is obtained at the last step of the entire code.

So Promise solves the writing structure of callbacks to some extent, but callbacks still exist on the main flow, just put them in then(…). Inside, there’s a disconnect with the sequential, linear logic of our brains.

What I want to focus on here is how promises address the lack of trust caused by inversion of control.

First of all be clear, Promise can ensure the following situation, citing the JavaScript | MDN:

  1. The callback function is never called until the current run of the JavaScript event queue completes
  2. Callbacks added via.then, or even functions added after the asynchronous operation has completed, are called
  3. With multiple calls to.then, you can add multiple callback functions that run independently in the order they were inserted

Let’s talk about trust issues that arise when callbacks are handled asynchronously, and whether these issues still exist when promises are implemented in full compliance with the PromiseA+ specification.

Call early

When using a callback function, we cannot guarantee or know what form the callback function will be called by a third party. If it is called synchronously and immediately in some cases, it may cause logic errors in our code.

However, according to the PromiseA+ specification, promises don’t have to worry about this, because even immediate promises (similar to new promises (function (resolve, reject) {resolve(2); }), and cannot be observed synchronously.

That is, call then(…) on a Promise. Even if the Promise has already been promised to then(…) The callback will always be called after the current run of the JavaScript event queue is complete, that is, asynchronously.

Call too late

Resolve (…) is called when a Promise creates an object. Or reject (…). When, this Promise is passed then(…) The registered callback is triggered at the next asynchronous point in time.

Also, multiple passes on this Promise then(…) Registered callbacks are called at the next asynchronous point in time, and none of these callbacks can affect or delay calls to other callbacks.

Examples are as follows:

p.then(function () { p.then(function () { console.log('C'); }); console.log('A'); }) .then(funtion () { console.log('B'); }); // Print A, B, and CCopy the code

As you can see from this example, C cannot interrupt or preempt B, so promises are never called too late, as long as you register THEN (…). “Must be called sequentially, because that’s how promises work.

The callback is not called

Nothing (not even a JavaScript error) prevents Promise from notifying you of its decision, if it ever does. If you register a success callback and a rejection callback for a Promise, the Promise will always invoke one of them when it makes the decision.

Of course, if your callback contains JavaScript errors, you might not see the results you expect, but the callback is called anyway.

p.then(function (data) { console.log(data); foo.bar(); Type Error, foo is not defined}, function (err) {});Copy the code

Too many or too few calls

According to the PromiseA+ specification, the correct number of callbacks should be one. “Too little” means no call, as explained above.

The “too much” situation is easy to explain, and promises are defined in such a way that they can only be made once. If for more than one reason, the Promise creates code that attempts to call resolve(…) multiple times. Or reject (…). “, or try to call both, and the Promise will only accept the first decision and silently ignore any subsequent calls.

Since promises can only be made once, anything that passes then(…) The registered callback is called only once.

Failed to pass parameter value

If you do not pass any value to resolve(…) Or reject (…). , then the value is undefined. But whatever this value is, it is passed to all registered in then(…). The callback function in.

If resolve(…) is called with multiple arguments Or reject (…). All parameters after the first parameter are ignored. If you pass multiple values, you must encapsulate them in a single value, such as an array or object.

Swallow possible errors or exceptions

If a JavaScript exception error, such as a TypeError or ReferenceError, occurs at any point during the creation of a Promise or during the viewing of its resolution results, the exception will be caught and the Promise will be rejected.

Examples are as follows:

var p = new Promise(function (resolve, reject) { foo.bar(); // foo does not define resolve(2); }); p.then(function (data) { console.log(data); }, function (err) {console.log(err); // Err will be a TypeError exception object from foo.bar()});Copy the code

The JavaScript exception that occurs in foo.bar() causes the Promise to be rejected, and you can catch and respond to it.

Not all Thenable can be trusted

So far, we’ve discussed how using promises can avoid many of the above trust problems caused by inversion of control. However, as you’ve no doubt noticed, Promise doesn’t get rid of callbacks entirely, it just changes where they’re delivered. We’re not passing the callback to Foo (…) Let a third party do it, but from Foo (…) Get something (the Promise object) and pass the callback to it.

But why is this more trustworthy than just using callbacks? How can you be sure that the thing returned is actually a trusted Promise?

Promise already has a solution to this problem, and ES6 implements the Promise solution promise.resolve (…) .

If the Promise. Resolve (…). Pass a non-promise, non-thenable immediate value, and you’ll get a Promise populated with that value.

Examples are as follows:

var p1 = new Promise(function (resolve, reject) { resolve(2); }); var p2 = Promise.resolve(2); // P1 and p2 have the same effectCopy the code

Resolve (…) Passing a real Promise will only return the same Promise.

var p1 = Promise.resolve(2);
var p2 = Promise.resolve(p1);

p1 === p2;    // trueCopy the code

More importantly, if the promise.resolve (…) Passing a non-promise thenable value, the former attempts to expand the value until a concrete non-promise final value is extracted.

Examples are as follows:

var p = { then: function (cb, errCb) { cb(2); errCb('haha'); }}; P.chen (function (data) {console.log(data); // This works because functions are first-class citizens and can be passed as arguments. // 2 }, function (err) { console.log(err); // haha });Copy the code

This p is a Thenable, but it’s not really a Promise. It doesn’t behave exactly like a Promise. It triggers both a success callback and a rejection callback, and it can’t be trusted.

Nevertheless, we can all pass this p to promise.resolve (…). , and then the expected normalized security result will be obtained:

Promise.resolve(p) .then(function (data) { console.log(data); // 2 }, function (err) { console.log(err); // Will never get here});Copy the code

As discussed earlier, a Promise accepts only one resolution if resolve(…) is called multiple times. Or reject (…). , the ones that follow are automatically ignored.

Promise.resolve(…) You can take any Thenable and decapsulate it to its non-Thenable value. From the Promise. Resolve (…). You get a real Promise, a value you can trust. If you pass in a real Promise, you get it itself, so promise.resolve (…) It doesn’t hurt at all to filter for credible capriciousness.

In summary, it is clear that using Promise to handle asynchrony can solve a number of trust issues associated with inversion of control for callback functions. Good. We took another step forward.

Step3 – Generator Gererator

In Step1, we identified two key issues for expressing asynchronous processes with callbacks:

  1. Callback-based asynchrony does not fit the way the brain regulates the steps of a task
  2. Callbacks are not trusted due to inversion of control

In Step2, we detailed how Promise reverses the control of the callback and then reverses it, restoring the trusted willfulness.

Now we turn our attention to a sequential, seemingly synchronous, asynchronous process control presentation style, the Gererator in ES6.

Iterable protocol and iterator protocol

Before you understand Generator, you must understand two new protocols in ES6: the iterable protocol and the iterator protocol.

Iterable protocol

Iterable protocols run JavaScript objects to define or customize their iterative behavior, such as (define) in a for… What values in the of structure can be looped. The following built-in types are built-in iterables and have the default iterative behavior:

  1. Array
  2. Map
  3. Set
  4. String
  5. TypedArray
  6. The Arguments object for the function
  7. The NodeList object

Note that Object does not comply with the iterable protocol.

To become iterable, an object must implement the @@iterator method, which means that the object (or some object in its prototype chain) must have a property named symbol. iterator:

attribute value
[Symbol.iterator] A nonparametric function that returns an object conforming to the iterator protocol

When an object needs to be iterated over (such as starting for a for… Of), whose @@iterator method is called with no arguments and returns an iterator used to get the value in the iteration.

Iterator protocol

The iterator protocol defines a standard way to produce a finite or infinite sequence of values. When an object is considered an iterator, it implements a next() method and has the following meanings:

attribute value
next A nonparametric function that returns an object with two attributes:

1. Done (Boolean)

– True if the iterator has already passed through the iterated sequence. In this case, value may describe the return value of the iterator

– False if the iterator can produce the next value in the sequence. This is equivalent to not specifying it along with the done attribute.

2. value– Any JavaScript value returned by the iterator. If done is true, you can ignore it.

Examples of using the iterable and iterator protocols:

var str = 'hello'; // The iterable protocol uses for... Of accesses typeof STR [symbol.iterator]; // 'function' for (var s of str) { console.log(s); Var iterator = STR [symbol.iterator](); // Print 'h', 'e', 'l', 'l', 'o'} iterator.next(); // {value: "h", done: false} iterator.next(); // {value: "e", done: false} iterator.next(); // {value: "l", done: false} iterator.next(); // {value: "l", done: false} iterator.next(); // {value: "o", done: false} iterator.next(); // {value: undefined, done: true}Copy the code

We implement an object ourselves that conforms to the iterable and iterator protocols:

var something = (function () { var nextVal; Return {// Iterable protocol for... Of consumption [symbol.iterator]: function () {return this; Nextfunction () {nextVal === undefined) {nextVal = 1; } else { nextVal = (3 * nextVal) + 6; } return {value: nextVal, done: false}; }}; }) (); something.next().value; // 1 something.next().value; // 9 something.next().value; // 33 something.next().value; / / 105Copy the code

Implement asynchrony with Generator

What if we used Generator to override the above callback nesting example? See the code:

function getKey () { $.ajax({ type: 'get', url: 'http://localhost:3000/apiKey', success: function (data) { key = data; it.next(key); } error: function (err) { console.log(err); }}); } function getToken (key) { $.ajax({ type: 'get', url: 'http://localhost:3000/getToken', data: { key: key }, success: function (data) { loginData = data; it.next(loginData); } error: function (err) { console.log(err); }}); } function getData (loginData) { $.ajax({ type: 'get', url: 'http://localhost:3000/getData', data: { token: loginData.token, userId: loginData.userId }, success: function (busiData) { it.next(busiData); } error: function (err) { console.log(err); }}); } function *main () { let key = yield getKey(); let LoginData = yield getToken(key); let busiData = yield getData(loginData); Console. log(' Business data: ', busiData); } // generate an iterator instance var it = main(); // Run the first step it.next(); Console. log(' does not affect main thread execution ');Copy the code

Note that the code inside the *main() generator, regardless of the yield keyword, is written synchronously in the same way that the brain is used to, encapsulating the asynchronous process outside, calling it.next() in the successful callback function, queuing the returned data into a task queue, When the main JavaScript thread is idle, callback tasks are pulled from the task queue to execute.

There is no time to execute tasks in the task queue if we keep using the main JavaScript thread:

// Run the first step it.next(); While (1) {}; // There is no chance to fetch the task from the task queueCopy the code

In summary, the Generator solves the first problem with callbacks handling asynchronous processes: the sequential, linear way the brain thinks.

Step4 – Async/Await

Above we introduced Promise and Generator. Combine the two to make Async/Await.

The disadvantage of Generator is that it also requires manual control of next() execution. If an Async/Await Await is followed by a Promise, resolve(…) will automatically wait until the Promise is returned. Or reject (…). Will do.

Let’s rewrite the original example with Async/Await:

let getKeyPromise = function () { return new Promsie(function (resolve, reject) { $.ajax({ type: 'get', url: 'http://localhost:3000/apiKey', success: function (data) { let key = data; resolve(key); }, error: function (err) { reject(err); }}); }); }; let getTokenPromise = function (key) { return new Promsie(function (resolve, reject) { $.ajax({ type: 'get', url: 'http://localhost:3000/getToken', data: { key: key }, success: function (data) { resolve(data); }, error: function (err) { reject(err); }}); }); }; let getDataPromise = function (data) { let token = data.token; let userId = data.userId; return new Promsie(function (resolve, reject) { $.ajax({ type: 'get', url: 'http://localhost:3000/getData', data: { token: token, userId: userId }, success: function (data) { resolve(data); }, error: function (err) { reject(err); }}); }); }; async function main () { let key = await getKeyPromise(); let loginData = await getTokenPromise(key); let busiData = await getDataPromise(loginData); Console. log(' Business data: ', busiData); } main(); Console. log(' does not affect main thread execution ');Copy the code

As you can see, using Async/Await is completely synchronous writing, logic and data dependencies are very clear, you just need to wrap asynchronous things with Promise and call them with Await. There is no need to manually control next() execution like Generator.

Async/Await is a combination of Generator and Promise that completely solves two problems with callback-based asynchronous processes and is probably the best JavaScript way to handle asynchracy right now.

conclusion

This article describes the development of asynchronous JavaScript programming in four stages:

  1. The first stage – the callback function, but causes two problems:

    • Lack of orderality: The debugging difficulties caused by callback hell do not correspond to the way the brain thinks
    • Lack of trust Capriciousness: A series of trust problems caused by inversion of control
  2. The second stage, Promise, is based on the implementation of the PromiseA+ specification, which solves the trust problem caused by the inversion of control and restores the initiative of code execution.
  3. The third stage – Generator function, using Generator, allows us to write code synchronously, which solves the sequential problem, but requires manual control of next(…). To send the data returned by the successful callback back to the JavaScript main flow.
  4. Phase 4 – Async/Await, Async/Await combines Promise and Generator, followed by Await a Promise, which automatically waits for the Promise’s resolution value, eliminating the need for the Generator to manually control next(…) The execution problem really implements writing asynchronous code in a synchronous manner.

We can see that each technical breakthrough is in order to solve some problems of the existing technology, it is a step by step, we are in the process of learning, to really understand the technology which solves the pain points, and why it exists, it would be good for our build systematic knowledge, at the same time, it would be better to understand the technology.

Finally, I hope you can have a more macroscopic and systematic understanding of JavaScript asynchronous programming through this article, and we will make progress together.

Reference:

  1. developer.mozilla.org…