Have you ever considered that you might be writing code that you can’t predict yourself, and this is often the case when another “black box” (third-party libraries or code that you’ve packaged) is involved? Of course, it would be nice if the black box ran synchronously, as long as the logic inside the black box was correct, it wouldn’t be too much of a problem. However, if the “black box” internal asynchronous code, then there will be trouble, because you may not know how the “black box” internal for asynchronous processing, even know, “black box” internal handling of the callback function will make you have to follow, because you can’t control the “black box” to the processing of the callback function. Of course, this is just the case with simply passing in callback functions, not promises…

What is control?

When it comes to control, it’s hard not to talk about JavaScript’s single-threading and asynchronous nature, but I’ll explain it in detail.

Single-threaded JavaScript

The JavaScript language itself is single-threaded, meaning that “only one thread executes JavaScript code,” but modern browsers simply execute JavaScript code, After all, JavaScript itself has no web library, no ability to monitor the browser, and no timer of its own to accomplish rich browser interactions.

How can I do that? Here’s what browsers do: Since JavaScript itself does not have a network library, the browser opens a thread to request network data. The JavaScript execution thread only needs to tell the network thread what function to call after the data is requested, and the network thread takes the data as the parameter of the function after the data is requested, and puts the function into the “event loop queue”. This function waits for the JavaScript thread to call it back (hence the name of the function callback). All of this is represented by an API that happens whenever the JavaScript execution thread calls the API.

To further understand, let’s take a tangible example:

Function apple() {setTimeout(function foo () {console.log('apple')}, 1000)} apple(Copy the code

First, the browser opens a thread for JavaScript execution, and the code is executed in this thread: Define the Apple function, call the Apple function, call the browser-exposed API setTimeout (for now, let’s mark that location), and the JavaScript thread executes the rest of the code.

Then, at the position marked above, the browser opens a timer thread in the browser based on the API called in the JavaScript execution thread. This thread is used to time the browser. After 1000ms, This thread is going to put foo in a place called an event loop queue.

Finally, the JavaScript execution thread will come back and call foo.

It is the collaboration between many different threads in the browser and JavaScript execution threads that makes for colorful interactions.

Transfer of control

From the above description, we can see some problems, to complete an asynchronous process, need to pass through at least two threads:

  1. The network request process passes through the JavaScript execution thread — the network request thread — the JavaScript execution thread
  2. The timing callback process passes through the JavaScript execution thread — timer thread — JavaScript execution thread

The status of JavaScript execution thread is controlled by our code. The network request thread and timer thread are beyond our control and controlled by the browser. Therefore, the above process is actually the process of control transfer. We refer to state changes like the transfer of control from JavaScript execution threads to network request threads as “inversion of control.” If we do not check the callback, then the code is actually out of control. After all, we do not have control, and if other threads make mistakes, you will be confused and follow the error, such as timer error, the callback function will not be called on time. This is also the problem with simply using callback functions!

In a broad sense, every one in the process, the loss of process control, all can be considered to transfer control, for example, you use a third-party library, you use his module encapsulation, may do what you don’t have clear internal processing, at that time, when you call these things, you will lose control of the code, Losing control of your code is scary, and it means you can’t predict how your code will perform. It’s better if you’re using a third-party library or module that is synchronous, because at least you can determine whether the library or module is reliable from the input and output aspects. After all, synchronous libraries or modules have unique inputs and can determine unique outputs because JavaScript is single-threaded, so it’s pretty easy to judge. However, if your library or module is asynchronous, it is very difficult to control, and your control falls into the hands of those libraries.

The tragedy of losing control

Since losing control to synchronous libraries and modules is less tragic, let’s focus on what happens when control falls to asynchronous libraries and modules, and of course, there are ways to take it back. First, the asynchronous library or module must pass in a “callback function”, and this “callback function” will be called from the asynchronous library or module. The logic of the call is unknown, because the control of the call is in the library or module. Second, if something goes wrong with the callback call, we have no control over it. Finally, if the asynchronous library sometimes behaves synchronously, we will suffer too!

Calling the callback function too early

When does this happen? Let’s simulate:

Var cache = 'apple' /** * * @param {string} url * @param {Function} callback * @return {string} returns data */ var ajax = function ajax (url, Callback) {if (url === 'a.com') {if (cache) {callback(cache)} else {setTimeout(function apple () { Callback (cache = 'apple')}, 1000)}}} / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * / / / formal code ajax (' a.com ', Function (response) {console.log(name + response)}) //... Var name = 'My name is'Copy the code

We thought library Ajax was always going to be asynchronous, but we didn’t think it was going to be cached internally. So, in this case, it’s going to be calling the callback synchronously, and the callback was called prematurely because we didn’t get the value of name, So it prints Apple instead of what we think my name is Apple. This is just a simple example, but it happens all the time: We want to call a variable in the parent scope inside the callback function, but can you make sure that variable is already in the parent scope? Can you ensure that this callback is not called prematurely? You have no idea what an asynchronous library will do.

Of course, you can avoid this problem by modifying the code for the callback function:

Ajax ('a.com', function (response) {// We consider this asynchronous execution, SetTimeout (function () {console.log(name + response)}, 0)})Copy the code

What ugly code, but it’s a good idea to take back control: I don’t know if you’re async, but I’ll async myself when I call back. I think you can solve this problem with Promise:

new Promise((resolve, reject) => { ajax('a.com', Function (response) {resolve(response)})}) then((res) => {console.log(name + res)}) (err) => { console.log(err) })Copy the code

Whether a library or module returns a result asynchronously or synchronously, a callback returns a Promise. Methods registered with THEN will always be called asynchronously. Helps us solve the “call too soon” problem.

Of course, the code above is still pretty ugly, and it would be much better if Ajax itself supported Promises. Of course, for libraries that don’t support Promises, you can write your own function to convert them into libraries that support Promises. Typically, third-party Promise libraries will have promise.wrap to provide transformations.

Call the callback function repeatedly

Suppose a library has a mechanism that sends out a request and sets a timer. If no data is returned after 100ms, it sends out another request. If any requests return, cancel the timer. Then it is possible for the callback function to be called twice! Let’s simulate it:

Var ajax = function ajax (url, Callback) {var delay = math.random () * 200 if (url === 'a.com') {// Set timeout timer var number = SetTimeout (function () {// request ajax(url, callback)}, SetTimeout (function () {callback('apple')}, Delay)}} / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * / / / / / formal code you trusted the library, Function (res) {console.log(res)})Copy the code

Control is in the hands of the library and the network thread. You must take control back and not leave the result uncertain. You can write:

Var isCall ajax('a.com', function (res) {if (! IsCall) {console.log(res) // isCall = true}})Copy the code

Finally made it unique. Wait, can Promise fix this for me? Sure, you just need the same code as above (I won’t paste it here), why? Because once a Promise is made, its value doesn’t change and it won’t be made again, your callback will only be executed once! (I don’t want to go into details about Promise because I assume you know how to use it.)

Delay in calling the callback causes a hang

Sometimes we have a callback that never gets called. What happens? This is because your library does not timeout: when the network request thread does not respond, and the library does not timeout, asynchronous calls in the callback function will never be executed. The loss of control is a tragedy:

Var ajax = function (url, Callback) {if (url === 'a.com') {setTimeout(function () {callback('apple')}, 100000000000000000)}} / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * / / / formal code ajax (' a.com ', Function (res) {console.log(res)})Copy the code

We still need to take back control, even if the callback hasn’t been called yet, but the time we specified has passed, we need to make the callback meaningless. Let’s deal with this with Promise:

// Define a timing function, Var timeOuted = function (time) {return new Promise(resolve, Reject) {setTimeout(function () {reject(' reject ')}, time)})} Var request = function(url) {return new Promise(function(resolve, reject) {ajax(url, Var p1 = request('a.com') var p2 = timeOuted(1000) var p2 = timeOuted(1000 Promise when race. Race (/ p1, p2). Then ((res) = > {the console. The log (res)}, (err) = > {the console. The log (err)})Copy the code

The promise. race array contains many individual promises. If one Promise is decided, all other promises are ignored, and promise. race returns the Promise that was decided first. The then method calls the callback asynchronously, effectively preventing timeouts. In fact, the self-created timeout function is also an asynchronous function, and the case of two asynchronous threads running at the same time is called “parallel”, which will be discussed later.

An error occurred while calling the callback function

When a library or module calls a callback function, it is possible to make a callback error if the library misjudges the return parameters or imposes improper restrictions. A more common error is when a callback is called, and the library hides the error. The callback failed, but you can’t see it from the outside, so you don’t need to simulate it, but you need some tricks to avoid it. Of course, you can use promises to get around these problems. Why? Because promises require you to have a resolution, promises have error passing mechanisms, and the Promise resolution can only be a single value: resolve(1, 2, 3) or resolve(1); Resolve (2) : resolve(1); resolve(2) : resolve(1);

conclusion

Control is often not in the code we write during asynchrony. Callbacks don’t help us solve this problem natively, but promises do a great job of taking back control and making code predictable, which is one of the reasons we use promises. At this point, we’ve been talking about a single asynchronous process, and of course we’ve found that promises play a very important role in this process, but they do a lot more than that. They work well with multiple asynchronous threads in parallel, too.