For other translations of this series, see JS working mechanism – Xiaobai 1991 column – Nuggets (juejin. Cn)

This article is shared under a Creative Commons Attribution 4.0 International license, BY Troland.

This is chapter 4 of how JavaScript works.

Why do you need a single-thread limit?

In Chapter 1, we discussed what happens when there are long-running function calls in the call stack.

Suppose we run a complex image transformation algorithm in a browser.

When the call stack execution has a function that needs to be executed, the browser can’t do anything else — it blocks. The browser won’t render, won’t run any code, and your UI is completely stuck.

Once your browser starts processing too many tasks in the call stack, it will stop responding for a long time. At that point, many browsers will raise an exception warning asking if the page needs to be closed. This reminder is ugly:

JS program building blocks

You may write programs in a single JS file, but your programs typically consist of many blocks, and only one block is executing at a time, with the rest executed later. The most common block unit is a function. Most developers who are new to JS seem to have an understanding that ‘later’ doesn’t happen immediately after now. In other words, tasks that cannot currently be completed will be completed asynchronously so that you don’t inadvertently encounter the UI blocking behavior mentioned above.

var response = ajax('https://example.com/api'); console.log(response); // 'response' will have no data returnedCopy the code

Standard AJAX requests do not complete synchronously, which means that the code in the AJAX method does not yet return any value to the response variable. An easy way to wait for an asynchronous method to return is to use a callback function

ajax('https://example.com/api', function(response) { console.log(response); // 'response' is now available});Copy the code

You can make a synchronous Ajax request entirely. But never do it. If you make a synchronous request, your JS application’s UI will block, and the user can’t click, enter data, navigate, or scroll. This will freeze any user’s interaction experience, which is bad.

/ / assume that you are using jQuery jQuery. Ajax ({url: 'https://api.example.com/endpoint' success: function (response) {/ / write callback} here, async: False // Sync mode});Copy the code

This code looks like this, but don’t ever do this, don’t ruin your Web.

As an example, we can put any code into asynchronous execution.

This can be done with the setTimeout(callback, milliseconds) function. What the setTimeout function does is to set up an event (a timeout) to happen later.

SetTimeout can do this. What setTimeout does is set an event to be executed later.

function first() {
    console.log('first');
}
function second() {
    console.log('second');
}
function third() {
    console.log('third');
}
first();
setTimeout(second, 1000); // Invoke `second` after 1000ms
third();
Copy the code

We should get output like this:

first
third
second
Copy the code

What is an Event Loop?

We start with a slightly odd claim that while it is possible to make JS code asynchronous (such as setTimetout discussed earlier) up to ES6, JavaScript itself does not actually integrate any direct asynchronous programming concepts. The JavaScript engine only allows a single program fragment to be executed at any time.

For more details on JS engines, see the previous chapter.

So, who tells the JS engine to execute the snippet? In fact, the JS engine does not execute in isolation, it runs in a host environment (Chrome or Node). In fact, today, JS is embedded in almost every type of hardware, from robots to light bulbs. Each device provides a different hosting environment for the JS engine.

All host environments have a built-in mechanism called an event loop that executes multiple snippets of code in the program over time, calling the JS engine each time.

This means that the JS engine is just an on-demand environment. This is a closed environment in which events are scheduled (running JS code).

So, for example. When your application makes a request to fetch data from the server, it sets a callback function. The JS engine tells the host environment ‘Hi, I’m going to suspend running for a while, when you complete the network request and get the data, please call the callback function’.

The browser then sets up a listener for network responses, and when data is returned, it adds a callback function to the EventLoop. Take a look at the schematic

What is a Web API? Essentially, you don’t have access to these threads, you can only call them. They are native to the browser and can be operated concurrently in the browser. If you’re a node. js developer, these are the C++ APIs.

What exactly is an EventLoop?

EventLoop has only one simple job – monitoring the call stack and callback queue. If the call stack is empty, it gets the first event from the callback queue and pushes it in, then executes.

Such a traverse is called a tick. Each event is a callback function.

console.log('Hi');
setTimeout(function cb1() { 
    console.log('cb1');
}, 5000);
console.log('Bye');
Copy the code

Let’s execute this code and see what happens:

1. Empty state. The browser console is empty, and the call stack is empty.

2. The console. The log (‘ Hi ‘) into the stack.

3. Run console.log(‘Hi’).

4. The console. The log (‘ Hi ‘) out of the stack

  1. setTimeout(function cb1() { ... })Into the stack.

6. Run setTimeout(function cb1() {… }, the browser creates a timer as part of the web API and will handle the countdown for you.

7.setTimeout(function cb1() { … }) completes and exits the stack.

8. The console. The log (‘ Bye ‘) into the stack.

9. Run console.log(‘Bye’).

10. The console. The log (‘ Bye ‘) out of the stack.

11. After at least 5 seconds, the timer stops running and the CB1 callback is added to the callback queue.

12. The event loop retrieves the CB1 function from the callback queue and pushes it onto the stack.

13. Run cb1 and push console.log(‘cb1’) to the stack.

14. Run console.log(‘cb1’).

15. The console. The log (‘ cb1) out of the stack.

16. Cb1 stack

ES6 specifies how EventLoop works, which means that EventLoop is technically within the responsibility of the JS engine, which is no longer just playing host to the environment.

The advent of Promises in ES6 was one of the main reasons for the change, as ES6 requires more direct and fine-grained control over scheduling operations in event loop queues (more on that later).

SetTimeout (…). How does it work?

SetTimeout does not automatically put your callback into the Loop queue. It sets a timer. When the timer runs out, the host environment places the callback into the EventLoop so it can be picked up and executed in the future.

setTimeout(myCallback, 1000);
Copy the code

This does not mean that it will be executed after 1000 ms, only that it will be added to the event queue. While other events have been added to the queue at this point, your callback can only wait. Now, you know how the Event Loop works, and you know how setTimeout works: Calling time with the argument 0 just suspends execution until the call stack is cleared.

console.log('Hi');
setTimeout(function() {
    console.log('callback');
}, 0);
console.log('Bye');
Copy the code

So even though I set the timer to zero, the output looks like this

Hi
Bye
callback
Copy the code

What is an ES6 Job?

A new concept, called Job queues (jobs in this case, micro tasks), was introduced in ES6. It is always at the top of the Event Loop queue. You most likely stumbled upon this concept while dealing with Promises(which I’ll talk about later) ‘s asynchronous behavior. Now that we’ve just touched on the concept, you’ll understand later how these behaviors are scheduled and handled, right

Imagine a scenario where the Job queue is attached to the queue at the end of each tick in the Event Loop queue. Some asynchronous operations generated by a tick in the Event Loop do not result in new events being added to the Event Loop queue, but instead add a job item to the end of the current tick’s job queue.

This means that you can add another function to be executed later, and you can be sure that it will be executed before any other function.

A Job can cause more jobs to be added to the end of the same queue. In theory, it could be a Job Loop, an infinite Loop of add events that prevents the program from getting the necessary resources to continue execution until the next tick. Conceptually, this is a bit like executing an infinite loop in code.

Jobs are kind of like the setTimeout(callback, 2) “hack” but implemented in such a way that they are introduced a much more defined and guaranteed ordering: Later, but as soon as possible. Job is a bit like setTimeout(callback, 0), but implemented differently. They have a well-defined and guaranteed order of execution: execute later, but still as quickly as possible

The callback

As you already know, callbacks are a very common way to express and manage asynchronous programming these days. Indeed, callback is the most important asynchronous pattern. Countless JS programs, even very complex and clever applications, are based on callbacks.

Callbacks have their drawbacks, though, and many developers are trying to find better asynchronous patterns. But it’s almost impossible to use any abstract syntax effectively without understanding how it works. In the following chapters, we’ll delve deeper into these abstract grammarias and understand the need for more complex asynchronous patterns.

Nested callbacks

Look at the code:

listen('click', function (e){ setTimeout(function(){ ajax('https://api.example.com/endpoint', function (text){ if (text == "hello") { doSomething(); } else if (text == "world") { doSomethingElse(); }}); }, 500); });Copy the code

There are three nested functions in the code, each representing an asynchronous step. This style of code is often referred to as’ callback hell ‘. Callback hell is not a nested/indented formatting problem, it has deeper problems.

First, we’re waiting for the ‘click’ event, then we’re waiting for a timer to fire, and then we’re waiting for the Ajax response again, which is repeated all over again. At first glance, it seems possible to split the asynchronous code above into step-by-step code, as follows:

listen('click', function (e) {
	// ..
});
Copy the code

then

setTimeout(function(){
    // ..
}, 500);
Copy the code

then

ajax('https://api.example.com/endpoint', function (text){
    // ..
});
Copy the code

The last

if (text == "hello") {
    doSomething();
}
else if (text == "world") {
    doSomethingElse();
}
Copy the code

There should be a way to represent asynchronous code in such a sequential way that it looks like it’s going to go without a break, right?

Promise

Check out the following code:

var x = 1;
var y = 2;
console.log(x + y);
Copy the code

This is straightforward: figure out the values of x and y, and print them out on the console. But what if the initial value of x or y is nonexistent or indeterminate? Suppose we need to get the values of x and y from the server before we can use x and y in our expression. Suppose the functions loadX and loadY get the values of x and y, respectively, from the server. Then, once you have the values of x and y, you can use the sum function to calculate the sum.

function sum(getX, getY, callback) {
    var x, y;
    getX(function(result) {
        x = result;
        if (y !== undefined) {
            callback(x + y);
        }
    });
    getY(function(result) {
        y = result;
        if (x !== undefined) {
            callback(x + y);
        }
    });
}
// A sync or async function that retrieves the value of `x`
function fetchX() {
    // ..
}


// A sync or async function that retrieves the value of `y`
function fetchY() {
    // ..
}
sum(fetchX, fetchY, function(result) {
    console.log(result);
});
Copy the code

One thing to remember here – in the code snippet, x and y are future values and we use sum(..) Evaluates and (externally), but does not determine whether x and y have values at the same time immediately. This crude callback-based technique leaves a lot to be desired. This is just a small step to help you understand the benefits of using future values without worrying about when they will be available.

Promise value

Let’s look briefly at Promises: Promises x+ Y: Promises x+ Y: Promises x+ Y: Promises X + Y: Promises X + Y

function sum(xPromise, yPromise) { // `Promise.all([ .. ] Return Promise. All ([xPromise, yPromise]) // When the new Promise is resolved, I can take the values of x and y and add them together. . Then (function(values){// 'values' return values[0] + values[1]; }); } // 'fetchX()' and 'fetchY()' return a promise to get the respective return value, which is non-sequential. Sum (fetchX(), fetchY()) = sum(fetchX(), fetchY())) 'to handle the returned promise. .then(function(sum){ console.log(sum); });Copy the code

The code snippet above contains two layers of Promise.

FetchX () and fetchY() are direct calls, and their return values (promises!) Are passed sum(…) As a parameter. Promises may return values now or later, but each promise has the same asynchronous behavior. We can assume that x and y are time independent values. Let’s call them future values for now.

The second level of promise is made by sum(…) (through promise.all ([…] ) created and returned by calling then(…) To wait for the return value of the Promise. When the sum (…). When it’s done, it returns the future value of sum and then it can be printed out. We are in the sum (…). The logic of waiting for future values x and y is hidden inside.

Note: in sum(…) Inside, promise.all ([…] Creates a chain call of a promise(after waiting for the resolution of promiseX and promiseY),.then(…) Another promise is created, which is immediately resolved (returning the sum) by code VALUES [0] + values[1]. So, at the end of the snippet is sum(…) Then (…) – Actually operating on the second returned promise instead of the first returned promise. All ([…] ) creates the return Promise. Similarly, although we don’t have the second then(…) The chain call is then made, but it also creates another promise that we can choose to observe/use. We’ll explore the chain-call correlation of Promises in more detail later in this chapter.

In Promises, real then(…) A function can take two functions as arguments, the first a success function and the second a failure function.

Function (sum) {console.log(sum); }, // Reject handle function(err) {console.error(err); // bummer! });Copy the code

Error when getting x or y or calculating the sum value, sum(…) The returned promise will fail, passing in then(…) The callback error handler as the second argument will receive the return value from the Promise.

Because promises encapsulate a time-dependent state of waiting for an external return value of success or failure, promises themselves are time-independent, making it possible to compose (merge) promises in a predictable manner regardless of timing or return results.

In addition, once a Promise is resolved, it remains immutable and can be observed at will.

The chaining call to promise really works sometimes:

function delay(time) {
    return new Promise(function(resolve, reject){
        setTimeout(resolve, time);
    });
}

delay(1000)
.then(function(){
    console.log("after 1000ms");
    return delay(2000);
})
.then(function(){
    console.log("after another 2000ms");
})
.then(function(){
    console.log("step 4 (next Job)");
    return delay(5000);
})
// ...
Copy the code

Call Delay (2000) to create a promise that will return success in 2 seconds, and then, from the first then(…) Returns the promise in the success callback function of the The returned promise waits 2 seconds to return a successful promise.

Note: Because once a promise ends, its internal state cannot be changed externally, it is safe to distribute state values to any third party. This is especially true when it comes to observing the return of a Promise from multiple sources. Immutability may sound like an obscure scientific topic, but it’s actually a fundamental and important aspect of Promise that you need to work on.

Promise use time

An important detail of promises is determining whether certain values are real promises. In other words, does this value behave like a Promise?

We can use new Promise(…) Syntax to create promises, and then you might think of using p instanceof Promise to check if an object is an instanceof the Promise class. However, this is not entirely true.

The main reason is that you can get a Promise instance from another browser window (such as iframe), and a Promise in an IFrame is different from a Promise in the current browser window or framework, thus causing detection of the Promise instance to fail.

In addition, the library or framework may choose to use its own Promise rather than the native ES6 Promise implementation. In practice, you can use the Promise that comes with the library to work with older browsers that don’t support Promises.

Exception handling

If a JavaScript error exception such as TypeError or ReferenceError is encountered at any point during the creation of a Promise or while observing the result returned from parsing the Promise, this exception is caught and enforces the Promise to fail.

Such as:

var p = new Promise(function(resolve, reject){ foo.bar(); // 'foo' is not defined, an error is generated! resolve(374); // never execute :(}); This is a big pity (){// This will be a big pity (). Function rejected(err){// 'err' will be a 'TypeError' exception object // because of 'foo.bar()' line});Copy the code

However, if the Promise resolves successfully and the listener (then(…)) resolves successfully What if a JS runtime error is thrown in the register callback? You can still catch the exception, but you’ll find it handled in a somewhat strange way:

var p = new Promise( function(resolve,reject){ resolve(374); }); p.then(function fulfilled(message){ foo.bar(); console.log(message); }, function rejected(err){// Never execute});Copy the code

It looks like the error exception thrown by foo.bar() was actually caught. But it didn’t, and we didn’t detect some of these deeper errors. P.t hen (…). The call itself returns another promise, which will handle the Rejiected handle.

To expand on the above description, this is not in the original text.

var p = new Promise( function(resolve,reject){ resolve(374); }); p.then(function fulfilled(message){ foo.bar(); console.log(message); Then (function() {}, function(err) {console.log('err', err); // Never execute}, function rejected(err){// Never execute}). });Copy the code

As shown in the code above, you can actually catch the error in the code that promise successfully resolved the callback function.

Handle uncaught exceptions

Some techniques are said to handle exceptions better.

99. The common way to add done(..) to Promises is to make Promises. Callback, which marks the state of the Promise chain as “done.” Done (…). No promise is created and returned, so done(..) is passed when no chained promise exists. The callback will not throw an error.

Same as uncaught error condition: any in done(..) Exceptions in failure handlers will be thrown as global errors (basically on the developer console).

var p = Promise.resolve(374); // Console. log(MSG. ToLowerCase ()); // console.log(MSG. }). Done (null, function() {// If an error occurs, a global error will be thrown});Copy the code

The Async/await ES8

JavaScript ES8 introduces async/await, which makes Promises much easier to handle. We will briefly cover all the possible positions of async/await and use them to write asynchronous code. Take a look at how async/await works.

Define an asynchronous function using the async function. This function returns an asynchronous function object. The AsyncFunction object represents running code in an asynchronous function.

When an asynchronous function is called, it returns a Promise. When an asynchronous function returns, the return value is not a Promise, but a Promise is automatically created and resolved using the return value. When the async function throws an exception, the Promise failure callback gets the raised exception.

An async function can contain an await expression so that it can suspend the execution of the function to wait for the return result of an incoming Promise, and then restart the execution of the asynchronous function and return the parse value.

You can think of promises in JavaScript as Future in Java or Task in C#.

Async /await is intended to simplify the use of Promises.

Take a look at this code:

Function getNumber1() {return promise.resolve ('374'); } async function getNumber2() {return 374; }Copy the code

Similarly, a function that throws an exception is equivalent to returning promises that fail.

function f1() {
    return Promise.reject('Some error');
}
async function f2() {
    throw 'Some error';
}
Copy the code

The await keyword can only be used in async functions and allows you to wait for promises synchronously. If promises are used outside async, we still have to use then callbacks.

Async function loadData() {// 'rp' is a function that initiates promises. var promise1 = rp('https://api.example.com/endpoint1'); var promise2 = rp('https://api.example.com/endpoint2'); // Now, two promises are requested concurrently, and now we must wait for them to finish running. var response1 = await promise1; var response2 = await promise2; return response1 + ' ' + response2; } // 'then' must be used because 'async function' is no longer used. loadData().then(() => console.log('Done'));Copy the code

You can also define asynchronous functions using asynchronous function expressions. Asynchronous function expressions have similar syntax to asynchronous function statements. The main difference between an asynchronous function expression and an asynchronous function statement is the function name, which can be ignored to create an anonymous function. Asynchronous function expressions can be used as IIFE(immediate execution function expressions) and can be run as soon as defined.

Like this:

Var loadData = async function() {// 'rp' is a function that initiates promises. var promise1 = rp('https://api.example.com/endpoint1'); var promise2 = rp('https://api.example.com/endpoint2'); // Now, two promises are requested concurrently, and now we must wait for them to finish running. var response1 = await promise1; var response2 = await promise2; return response1 + ' ' + response2; }Copy the code

More importantly, all major browsers support async/await

If this compatibility doesn’t meet your needs, you can use JS translators such as Babel and TypeScript to convert to your desired level of compatibility.

Finally, don’t blindly use the latest technology to write asynchronous code. It’s important to understand the inner workings of Async in JavaScript, but like everything else in programming, each technique has its pros and cons.

5 tips for writing highly usable, robust asynchronous code

1. Simplicity: Using async/await allows you to write less code. Each time you write async/await code, you can skip writing unnecessary steps such as not writing.then callbacks, creating anonymous functions to handle the return value, and naming the return value of the callback.

// 'rp' is a utility function that initiates promises. The rp (' https://api.example.com/endpoint1 '). Then (function (data) {/ /... });Copy the code

Contrast:

/ / var ` rp ` is a promise of tooling function response = await the rp (' https://api.example.com/endpoint1 ');Copy the code

2. Error handling: Async/await allows you to use everyday try/catch code constructs to handle synchronous and asynchronous errors. Take a look at how the Promise is written:

Function loadData() {try {getJSON(). Then (function(response) {var parsed = JSON. Parse (response); console.log(parsed); }).catch(function(e) {console.log(e); }); } catch(e) { console.log(e); }}Copy the code

Contrast:

async function loadData() { try { var data = JSON.parse(await getJSON()); console.log(data); } catch(e) { console.log(e); }}Copy the code

3. Conditional statements: It is more intuitive to write conditional statements with async/await.

function loadData() {
  return getJSON()
    .then(function(response) {
      if (response.needsAnotherRequest) {
        return makeAnotherRequest(response)
          .then(function(anotherResponse) {
            console.log(anotherResponse)
            return anotherResponse
          })
      } else {
        console.log(response)
        return response
      }
    })
}
Copy the code

Contrast:

async function loadData() { var response = await getJSON(); if (response.needsAnotherRequest) { var anotherResponse = await makeAnotherRequest(response); console.log(anotherResponse) return anotherResponse } else { console.log(response); return response; }}Copy the code

4. Stack frame: Unlike async/await, there is no way to know from the error stack returned by chained Promises where the error occurred. Look at the following code:

function loadData() {
  return callAPromise()
    .then(callback1)
    .then(callback2)
    .then(callback3)
    .then(() => {
      throw new Error("boom");
    })
}
loadData()
  .catch(function(e) {
    console.log(err);

// Error: boom at callAPromise.then.then.then.then (index.js:8:13)
});
Copy the code

Contrast:

async function loadData() {
  await callAPromise1()
  await callAPromise2()
  await callAPromise3()
  await callAPromise4()
  await callAPromise5()
  throw new Error("boom");
}
loadData()
  .catch(function(e) {
    console.log(err);
    // output
    // Error: boom at loadData (index.js:7:9)
});
Copy the code

5. Debug: If you use Promises, you’ll know that debugging them can be a nightmare. For example, if you set a breakpoint in a.then block and use a debug shortcut such as “stop-over”, the debugger will not move to the next.then block because the debugger will only step through the code.

With async/await you can step to the next await call just like synchronous code.

Writing asynchronous JavaScript code is important, not only for the program itself but also for the library.