- The original address: blog.sessionstack.com/how-javascr…
- The author Alexander Zlatkov
How JavaScript works: Rise of event loops and asynchronous programming + 5 better ways to code async/await
Welcome to Part 4 of this series, which is devoted to JavaScript and its building components. Along the way of identifying and describing the core elements, we also share some rules of thumb for building SessionStack, a JavaScript application that must be powerful and high-performance to be competitive.
Did you miss the first three chapters? You can find them here:
- An overview of the engine, the Runtime, and the Call stack – An overview of the engine, runtime, and call stack.
- Inside Google’s V8 Engine + 5 Tips on How to Write Optimized Code
- Memory Management + How to handle 4 Common Memory Leaks – How to handle 4 common Memory Leaks
This time, we’ll expand on our first chapter by reviewing the shortcomings of programming in a single-threaded environment and how to overcome them to build great JavaScript UIs. As always, at the end of this article we will share 5 tips on how to write more concise code using async/await.
Why is a single thread a limitation?
In Chapter 1, we thought about what happens when a function Call in the Call Stack takes a lot of time to process.
For example, suppose a complex image conversion algorithm is running in a browser.
When function is executing in the Call Stack, the browser cannot perform any other operations – blocked. This means the browser can’t render, can’t run any other code, and is stuck. That is, your App UI is no longer efficient, the page interaction is no longer smooth.
Your app is stuck.
In some cases, this may not be a fatal problem. But in some cases it can be a bigger problem. Once the browser starts processing too many tasks in the Call Stack, it can stop responding for a long time. At that point, many browsers will raise an error asking if the page should be terminated:
It’s ugly and totally breaks the user experience:
Part of a JavaScript program
You might write a JavaScript application in a single.js file, but your program will almost certainly be made up of several blocks, only one of which will be executed immediately and the rest later. The most common block unit is function.
The problem most developers new to JavaScript seem to have is what “doesn’t have to happen right away” means. In other words, tasks that cannot be completed now will, by definition, be completed asynchronously, which means that you will not have the blocking behavior described above as you would subconsciously expect or wish.
Let’s take a look at the following example:
// ajax(..) is some arbitrary Ajax function given by a library
var response = ajax('https://example.com/api');
console.log(response);
// `response` won't have the response
Copy the code
You probably know that standard Ajax requests don’t complete synchronously, which means that Ajax (…) The function does not yet have any value to return to assign to the response variable. An easy way to wait for an asynchronous function to return its result is to use a function called a callback:
ajax('https://example.com/api'.function(response) {
console.log(response); // `response` is now available
});
Copy the code
Note: You can actually make synchronous Ajax requests. Never do that. If you make synchronous Ajax requests, the user interface of your JavaScript application will be blocked.
Users will not be able to click, enter data, navigate or scroll. This will prevent any user interaction. This is a terrible operation.
It looks like it, but please don’t — don’t ruin the web experience:
// This is assuming that you're using jQuery
jQuery.ajax({
url: 'https://api.example.com/endpoint'.success: function(response) {
// This is your callback.
},
async: false // And this is a terrible idea
});
Copy the code
Let’s just take an Ajax request as an example. You can make any block of code execute asynchronously.
You can do this using the setTimeout(callback, msiseconds) function. The setTimeout function sets an event (timeout) to occur at a later time. Let’s take a look:
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
The console will output the following:
first
third
second
Copy the code
Profiling the event cycle
Although asynchronous JavaScript code is allowed (such as the setTimeout we just discussed), we start with the odd statement that until ES6, JavaScript itself never actually had any direct asynchronous concepts built into it.
The JavaScript engine never does anything other than execute a single block of the program at any given moment.
For a detailed look at how JavaScript engines work (specifically Google’s V8), check out one of our previous articles on the subject.
So, who tells the JS engine to execute your block? In fact, the JS engine does not run in isolation, but in a hosted environment, which for most developers is a typical Web browser or Node.js. In fact, JavaScript is now embedded in everything from robots to light bulbs. Each individual device represents a different type of managed environment for the JS engine.
Common to all environments is a built-in mechanism called Event Loop that handles the execution of multiple blocks in the program each time JS Engine is called.
This means that JS Engine is just an on-demand environment for any JS code. Scheduling the event (JS code execution) execution time is executed by the surrounding environment.
So, for example, when your JavaScript program makes an Ajax request to get some data from the server, you set the “response” code (” callback “) in the function, and JS Engin E tells the hosting environment: “Hey, I’m suspending execution for now, but as soon as you complete the network request and have some data, please call this function back.”
The browser is then set up to listen for responses from the network, and when there is something to return, the callback function is scheduled to execute by inserting it into the Event loop.
As shown in figure:
You can learn more about the memory heap and the Call Stack in the previous article.
What are these Web apis? Essentially, they are threads that you can’t access and that you can call on. They are the browser components where the concurrency is. If you’re a Node.js developer, these are ALL C ++ apis.
So what is an Event loop?
The event loop has a simple job: monitor the Call Stack and the callback queue. If the Call Stack is empty, it will grab the first event from the queue and push it into the Call stack, which will effectively run it.
Such an iterative action in the Event loop is called a tick, and each Event is just a callback function.
console.log('Hi');
setTimeout(function cb1() {
console.log('cb1');
}, 5000);
console.log('Bye');
Copy the code
Let’s execute the code above to see what happens:
- The browser status is cleared, the console is empty, and the Call Stack is empty
console.log('Hi')
Added to the Call Stack
console.log('Hi')
console.log('Hi')
Remove from the Call Stack
6.setTimeout(function cb1() { … }) is executed, and the browser creates a timer as part of the Web apis that handles the countdown.
setTimeout(function cb1() { ... })
console.log('Bye')
Added to the Call Stack.
console.log('Bye')
Be implemented.
console.log('Bye')
Remove from the Call Stack.
- After 5000ms, the timer completes and will
The callback callback
Push to the queue of the callback.
- The event loop will
cb1
Push from the callback queue to the Call Stack.
cb1
Be executed, and willconsole.log('cb1')
Push the Call Stack.
console.log('cb1')
Be implemented.
console.log('cb1')
Remove from the call stack.
cb1
Remove from the call stack.
Here’s a quick recap:
Interestingly, ES6 specifies how the event loop works, which means that technically it is within the responsibility of the JS engine and no longer just acts as a managed environment.
One of the main reasons for this change was the introduction of Promises in ES6, which requires access to direct, fine-grained control over scheduling operations on event loop queues (which we’ll discuss in more detail later).
setTimeout(...)
How it works
Be sure to note that setTimeout(…) Callbacks are not automatically placed in the event loop queue. It sets a timer. When the timer expires, the environment puts your callback into an event loop for future ticks to pick it up and execute, as shown in the following code:
setTimeout(myCallback, 1000);
Copy the code
This does not mean that myCallback will execute exactly at 1000 ms, but rather that myCallback will be added to the queue within 1000 ms. However, the queue may also contain other events that were added earlier – your callback will have to wait.
SetTimeout (callback, 0) is recommended for many articles and tutorials on getting started with JavaScript Async. Okay, now you know what the event loop does and how setTimeout works: Calling setTimeout with 0 as the second argument only delays the callback until the Call Stack is cleared.
Take a look at this code:
console.log('Hi');
setTimeout(function() {
console.log('callback');
}, 0);
console.log('Bye');
Copy the code
Although the wait time is set to 0 ms, the result in the browser console will be the following:
Hi
Bye
callback
Copy the code
In the ES6Jobs
What is?
A new concept called the “Jobs Queue” was introduced in ES6. It is the layer at the top of the event loop queue. You’re most likely to encounter Promises when dealing with asynchronous behavior (we’ll also discuss them).
Right now we are talking about Promises when we are talking about asynchronous behavior. In a moment, you will see how these actions are implemented and handled.
Imagine: the “Jobs Queue” is a Queue attached to the end of each scale in the Event Loop. Some asynchronous operations that may occur in one tick of the event loop do not result in a new event being added to the event loop queue, but in a job being added to the end of the job queue of the current tick.
This means that you can add additional functionality for later execution, and be assured that it will be executed immediately afterwards.
Jobs can also cause more jobs to be added to the end of the same queue. In theory, a Job “loop” (where a job keeps adding other JBS) could spin indefinitely, leaving the program without the resources necessary to get to the next event loop scale. Conceptually, this is similar to expressing long running or infinite loops in code (e.g. While (true)..) .
Jobs are a bit like the “hacks” of setTimeout(callback, 0), but implemented in such a way that they introduce more explicit and guaranteed sorting: a little later, but as soon as possible.
The callback
As you know, callbacks are by far the most common way to express and manage asynchrony in JavaScript programs. In fact, callbacks are the most basic asynchronous pattern in the JavaScript language. In addition to callbacks, countless JS programs, even very complex ones, are written on an asynchronous basis.
Except that the callback didn’t bring any disadvantages. Many developers are trying to find better asynchronous patterns. However, if you do not understand the essence, you will not be able to better understand and use more abstract patterns.
In the next chapter, we’ll delve into two of these abstractions to show why a more complex asynchronous pattern is necessary and even recommended (this will be discussed in a subsequent article).
Nested callbacks
Take a look at the following 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
We nested three functions, each representing a step in an asynchronous sequence.
This code is often referred to as “callback hell.” But “callback hell” really has little to do with nesting/indentation. This is a deeper problem.
First, we’re waiting for the “click” event, then we’re waiting for the timer to start, and then we’re waiting for the Ajax response to return, which might be repeated.
At first glance, this code seems to map it asynchronously to sequential steps, such as:
listen('click'.function (e) {
// ..
});
Copy the code
And then:
setTimeout(function(){
// ..
}, 500);
Copy the code
And then:
ajax('https://api.example.com/endpoint'.function (text){
// ..
});
Copy the code
Finally:
if (text == "hello") {
doSomething();
}
else if (text == "world") {
doSomethingElse();
}
Copy the code
So this sequential, asynchronous code representation seems more natural, doesn’t it? There must be a way, right?
Promises
Let’s look at the following code:
var x = 1;
var y = 2;
console.log(x + y);
Copy the code
It’s all very simple: add the values of x and y and print them to the console. But what if the value of x or y is missing and still to be determined?
Suppose we need to retrieve the values of x and y from the server before we can use them in our expression.
Suppose we have a function loadX and loadY that load the values of x and y from the server, respectively. And then, let’s say we have a function that sums up, loads the values of x and y and sums them up.
It might look like this (pretty ugly, isn’t it?). :
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 very important thing in the code is that we treat x and y as indeterminate values, and we represent the operation sum(…). (From the outside) don’t care if X or Y or both are available.
Of course, this callback-based approach leaves a lot to be desired. This is just the first step, and the benefit of considering uncertainty is that you don’t have to care if they’re actually usable.
Promise Value
Let’s look at how Promises are made to express X + Y:
function sum(xPromise, yPromise) {
// `Promise.all([ .. ] )` takes an array of promises,
// and returns a new promise that waits on them
// all to finish
return Promise.all([xPromise, yPromise])
// when that promise is resolved, let's take the
// received `X` and `Y` values and add them together.
.then(function(values){
// `values` is an array of the messages from the
// previously resolved promises
return values[0] + values[1]; }); }// `fetchX()` and `fetchY()` return promises for
// their respective values, which may be ready
// *now* or *later*.
sum(fetchX(), fetchY())
// we get a promise back for the sum of those
// two numbers.
// now we chain-call `then(...) ` to wait for the
// resolution of that returned promise.
.then(function(sum){
console.log(sum);
});
Copy the code
This code contains two promises.
Call fetchX() and fetchY() directly and pass the Promise they return to sum(…). . These promises indicate that the return may be immediately or later, but each Promise normalizes its behavior to be the same. We reason about x and y values in a time-independent way. These are the values that come later.
The second block of code handles sum(…) using.then. Return promise (through promise.all ([…] ), when sum(…) Our value has been calculated and printed out, and we hide the details of waiting for x and y to return
Note:
In sum (…). The Promise. All ([…]. Call to create a promise (waiting for promiseX and promiseY resolution). To link to. Then (…). The call to values [0] creates another promise that returns the result of values [0] + values [1].
Therefore, we will sum(…) .then(…) Is a promise generated by executing values [0] + values [1]
Promises, then(…) The call can actually have two callback functions, the first for implementation (as shown earlier) and the second for rejection:
sum(fetchX(), fetchY())
.then(
// fullfillment handler
function(sum) {
console.log( sum );
},
// rejection handler
function(err) {
console.error( err ); // bummer!});Copy the code
If there is a problem getting x or y, or if the addition process fails for some reason, sum(…) The returned promise is rejected, passed to then(…). The second callback error handling callback.
Because promises encapsulate externally a time-dependent state (resolve or Reject waiting for value), promises themselves are time-independent, so promises can be combined in predictable ways.
Also, once a Promise is resolved, it stays that way forever – it becomes an unchanging value
The chained call to promise is really useful:
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
The implementation of then must wait until the state of the previous promise becomes resolve/ ondepressing, so after another 2000ms, the implementation will be fulfilled after 2000ms.
Do you use Promise?
One important detail about promises is that you need to know if the value needs to be promised. In short, if the fetch behavior is like a Promise.
We know that promises are created by New Promise(…) So, you might think that p instanceof Promise would check for an instanceof a Promise, but it doesn’t.
The main reason is that you can receive a Promise from another environment (such as iframe with a separate Promise), that window has its own Promise, different from the Promise in the current window or frame, and that the check will not recognize the Promise instance.
In addition, a library or framework may choose to use Promises of its own rather than ES6 Promise implementations. In fact, you’ll probably be using Promises with libraries in older browsers that don’t have Promises at all.
Abnormally swallowed?
If a JavaScript exception error (such as TypeError or ReferenceError) occurs at any time when a Promise is created or implemented, the exception is caught, which forces the Promise to be rejected.
For example:
var p = new Promise(function(resolve, reject){
foo.bar(); // `foo` is not defined, so error!
resolve(374); // never gets here :(
});
p.then(
function fulfilled(){
// never gets here :(
},
function rejected(err){
// `err` will be a `TypeError` exception object
// from the `foo.bar()` line.});Copy the code
If a promise has been resolved and an exception is thrown in the Resolved callback, no error will be caught in the current two callbacks to.then().
var p = new Promise( function(resolve,reject){
resolve(374);
});
p.then(function fulfilled(message){
foo.bar();
console.log(message); // never reached
},
function rejected(err){
// never reached});Copy the code
The promise throws another rejected promise, which needs to be caught in the second.then().
Handle uncaught exceptions
There are more and better ways
A common way to add done at the end of Promises is to make Promises. , which effectively marks the Promise chain as “done.” done(…) No Promise is created and returned, so the callback is passed to Done (..) Will no longer throw redundant promises (promise:undefined)
done(…) Uncaught errors are handled globally:
var p = Promise.resolve(374);
p.then(function fulfilled(msg){
// numbers don't have string functions,
// so will throw an error
console.log(msg.toLowerCase());
})
.done(null.function() {
// If an exception is caused here, it will be thrown globally
});
Copy the code
What happened in ES8? Async/await
JavaScript ES8 introduces Async/await to make promises easier, and we’ll briefly take a look at the possibilities Async/await offers and how to use them to write asynchronous code.
Let’s see how Async/await works.
You can define async functions using asynchronous function declarations. This class of functions returns AsyncFunction objects. The AsyncFunction object represents an asynchronous function that executes the code contained in the function.
Call async function, which returns a Promise. When the asynchronous function returns a value other than a Promise, the Promise is automatically created and parsed along with the value returned by the function. When an asynchronous function throws an exception, the Promise will be thrown with the value Rejected.
An asynchronous function can contain await expressions that pause the execution of the function and wait for the result of the passed Promise, then resume the execution of the asynchronous function and return the parsed value.
You can think of promises in JavaScript as Java futures or C# tasks.
The purpose of Async/await is to simplify the behavior of using promises.
Let’s look at the following example:
// Just a standard JavaScript function
function getNumber1() {
return Promise.resolve('374');
}
// This function does the same as getNumber1
async function getNumber2() {
return 374;
}
Copy the code
The two functions are equivalent if the promise is rejected ();
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 function and allows you to wait synchronously on promises. If we use Promise outside of an asynchronous function, we still have to use and then call back:
async function loadData() {
// `rp` is a request-promise function.
var promise1 = rp('https://api.example.com/endpoint1');
var promise2 = rp('https://api.example.com/endpoint2');
// Currently, both requests are fired, concurrently and
// now we'll have to wait for them to finish
var response1 = await promise1;
var response2 = await promise2;
return response1 + ' ' + response2;
}
// Since, we're not in an `async function` anymore
// we have to use `then`.
loadData().then((a)= > console.log('Done'));
Copy the code
You can also define async functions using “async function expressions”. Async function expressions are very similar to async function statements and have almost the same syntax. The main difference is the function name, which can be omitted from an asynchronous function expression to create an anonymous function. Async function expressions can be used as IIFE (immediate call function expressions), which are run as soon as they are defined.
Like this
var loadData = async function() {
// `rp` is a request-promise function.
var promise1 = rp('https://api.example.com/endpoint1');
var promise2 = rp('https://api.example.com/endpoint2');
// Currently, both requests are fired, concurrently and
// now we'll have to wait for them to finish
var response1 = await promise1;
var response2 = await promise2;
return response1 + ' ' + response2;
}
Copy the code
More importantly Async/await is already supported by all major browsers:
Ultimately, it’s important not to blindly choose the “latest” approach to writing asynchronous code. You must understand the internals of asynchronous JavaScript, understand why it is important, and have an in-depth understanding of the internals of your chosen method. Each approach has its advantages and disadvantages in programming.
Five tips for writing highly maintainable, robust Async code
- Clean code: Using async/await allows you to write less code. Every time you use async/await, you are skipping unnecessary steps. For example, you need to create an anonymous function to handle the response, naming the response from the callback, for example:
// `rp` is a request-promise function.The rp (' HTTPS://api.example.com/endpoint1').then(function(data) {
/ /...
});
Copy the code
And:
// `rp` is a request-promise function.
var response = awaitThe rp (' HTTPS://api.example.com/endpoint1');
Copy the code
- Error handling: Async/await makes it possible to handle both synchronous and asynchronous errors using the same code structure (known as try/catch statements). Let’s look at Promises.
function loadData() {
try { // Catches synchronous errors.
getJSON().then(function(response) {
var parsed = JSON.parse(response);
console.log(parsed);
}).catch(function(e) { // Catches asynchronous errors
console.log(e);
});
} catch(e) {
console.log(e); }}Copy the code
And:
async function loadData() {
try {
var data = JSON.parse(await getJSON());
console.log(data);
} catch(e) {
console.log(e); }}Copy the code
- Conditional selection: It is more intuitive to write conditional expressions using 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
And:
async function loadData() {
var response = await getJSON();
if (response.needsAnotherRequest) {
var anotherResponse = await makeAnotherRequest(response);
console.log(anotherResponse)
return anotherResponse
} else {
console.log(response);
returnresponse; }}Copy the code
- Error stack information. The error returned from the Promise chain does not provide any information:
function loadData() {
return callAPromise()
.then(callback1)
.then(callback2)
.then(callback3)
.then((a)= > {
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
And:
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
-
Debugging: If you use promises, you know that debugging them can be a nightmare. For example, if you set a breakpoint inside the.then block and use a debugging shortcut (such as “stop-over”), the debugger will not move to the following.then because it only “step” by synchronizing the code. With async/await, you can step out await calls exactly like normal synchronous functions.
Writing asynchronous JavaScript code is important not only for the application itself, but also for the library.
For example, the SessionStack library logs everything in your Web application/website: all DOM changes, user interactions, JavaScript exceptions, stack traces, failed network requests, and debug messages.
All of this must happen in your production environment without affecting any user experience. We need to optimize the code a lot and make it as asynchronous as possible so that we can increase the number of events that the Event Loop is handling.
Not only Librar! When you replay the user session in the SessionStack, we have to render everything that happened in the user’s browser at the time of the problem, and we have to refactor the entire state to allow you to jump back and forth in the session timeline. To do this, we make heavy use of JavaScript Async.
Here’s a free program to get you started with sessionStatck
Information:
- Github.com/getify/You-…
- Github.com/getify/You-…
- Nikgrozev.com/2017/10/01/…