3. Crazy geek


The original:
https://www.valentinog.com/bl…


This article first send WeChat messages public number: front-end pioneer welcome attention, every day to you push fresh front-end technology articles


Have you ever wondered how browsers read and run JavaScript code? It may seem magical, but you can learn something about what’s going on behind the scenes. Let’s explore the language by introducing us to the wonderful world of JavaScript engines.

Open the browser console in Chrome, and then look at the ‘Sources’ TAB. You’ll see an interesting name called the Call Stack (in Firefox, you can see the Call Stack after you insert a breakpoint in the code) :

What is a Call Stack? It looks like there’s a lot going on, even if you’re only executing a few lines of code. In fact, JavaScript is not available right out of the box in all Web browsers.

There is one large component that compiles and interprets our JavaScript code: the JavaScript engine. The most popular JavaScript engine is V8, which is used in Google Chrome and Node.js, SpiderMonkey for Firefox, and JavaScriptCore, which is used in Safari/WebKit.

Today’s JavaScript engines are a remarkable piece of engineering, and while they don’t cover every aspect of the browser’s work, each engine has smaller pieces that work hard for us.

One of these components is the call stack, which runs our code along with the global memory and execution context. Are you ready to welcome them?

JavaScript engine and global memory

I think of JavaScript as both a compiled and interpreted language. Believe it or not, the JavaScript engine actually compiles your code before executing it.

Sounds amazing, doesn’t it? This trick is called JIT (just-in-time compilation). It’s a big topic in itself, and even one book isn’t enough to describe how JIT works. But now we can skip the theory behind compilation and focus on the execution phase, which is still interesting.

Let’s start with the following code:

var num = 2;

function pow(num) {
    return num * num;
}

If I ask you how do you handle the above code in the browser? What would you say? You might say “the browser reads the code” or “the browser executes the code.”

Reality is more nuanced than that. The first step is not for the browser but for the engine to read the snippet. The JavaScript engine reads the code, and when it hits the first line, it puts some references into global memory.

Global memory (also known as heap) is the area that JavaScript engines use to hold variable and function declarations. So going back to the previous example, when the engine reads the above code, the global memory is populated with two bindings:

Even if the examples are just variables and functions, consider that your JavaScript code runs in a larger environment: a browser or in Node.js. In these environments, there are a number of predefined functions and variables, known as globals. Global memory will take up more space than num and pow. Keep that in mind.

Nothing is being done at this point, but what if we try to run our function like this:

var num = 2;
function pow(num) {
    return num * num;
}
pow(num);

What’s going to happen? Now things get interesting. When a function is called, the JavaScript engine makes room for the other two boxes:

  • Global execution context environment
  • The call stack

Global execution context and call stack

In the previous section you saw how the JavaScript engine reads variable and function declarations, which end up in global memory (heap).

But now that we’ve executed a JavaScript function, the engine has to process it. How to deal with it? Every JavaScript engine has a basic component called the call stack.

The call stack is a stack data structure: this means that elements can come in from the top, but cannot leave the stack if there are some elements above them. That’s what JavaScript functions do.

When a function starts executing, it can’t leave the call stack if it gets stuck with some other function. Note that this concept helps to understand the statement “JavaScript is single-threaded.”

But now let’s go back to the example above. When this function is called, the engine pushes the function onto the call stack:

I like to think of the call stack as a stack of potato chips. If you haven’t eaten all the chips at the top first, you can’t eat the chips at the bottom! Fortunately, our function is synchronous: it’s a simple multiplication that gets the result very quickly.

At the same time, the engine allocates a global execution context, which is the global environment in which JavaScript code runs. This is what it looks like:

Imagine the global execution environment as an ocean in which JavaScript global functions swim like fish. How wonderful! But that’s only half the story. What if the function has some nested variables or one or more inner functions?

Even in the following simple variants, the JavaScript engine creates a local execution context:

var num = 2;
function pow(num) {
    var fixed = 89;
    return num * num;
}
pow(num);

Notice that I added a variable named fixed to the function pow. In this case, the local execution context contains a box to hold fixed. I’m not very good at drawing smaller boxes in smaller boxes! You must use your imagination now.

The local execution context appears near the POW and is contained in the green box within the global execution context. You can also imagine that for each nested function within a nested function, the engine creates more local execution contexts. These boxes can get to where they need to go very quickly.

Single-threaded JavaScript

We say JavaScript is single-threaded because there is a call stack that handles our function. That is, if there are other functions waiting to be executed, a function cannot leave the call stack.

This is not a problem when dealing with synchronous code. For example, the sum of two numbers is calculated synchronously and in microseconds. But when it comes to network communication and interaction with the outside world?

Fortunately, JavaScript engines are designed to be asynchronous by default. Even if they can execute a function one at a time, there is a way for an external entity to execute a slower function: in our case, the browser. We’ll talk about that later.

At this point, you should know that when the browser loads some JavaScript code, the engine reads it line by line and performs the following steps:

  • Populate global memory (heap) with variable and function declarations
  • Each function call is sent to the call stack
  • Creates a global execution context in which global functions are executed
  • Creates many tiny local execution contexts (if there are internal variables or nested functions)

At this point, you should have a complete picture of the JavaScript engine synchronization mechanism in your head. In the next section, you’ll see how and why asynchronous code works in JavaScript.

Asynchronous JavaScript, callback queues, and event loops

Global memory, execution context, and call stack explain how synchronous JavaScript code works in the browser. But there is something else we are missing. What happens when you have an asynchronous function running?

By asynchronous functions I mean functions that take some time to complete each interaction with the outside world. For example, calls to REST APIs or calls to timers are asynchronous, as they may take several seconds to complete. Today’s JavaScript engines have a way of handling such functions without blocking the call stack, and so do browsers.

Keep in mind that the call stack can only execute one function at a time, and even a blocking function can freeze the browser directly. Fortunately, the JavaScript engine is very intelligent and can solve the problem with the help of the browser.

When we run an asynchronous function, the browser accepts the function and runs it. Consider the following timer:

setTimeout(callback, 10000); function callback(){ console.log('hello timer! '); }

You’ve no doubt seen setTimeout many times, but what you may not know is that it’s not a built-in JavaScript function. That is, when JavaScript was born, there was no built-in setTimeout in the language.

SetTimeout is actually part of the so-called Browser API, which is a collection of convenience tools that browsers provide to us. How thoughtful! What does this mean in practice? Since SetTimeout is a browser API, this function is run directly by the browser (it appears temporarily in the call stack, but is deleted immediately).

Then 10 seconds later the browser accepts the callback function we pass in and moves it to the callback queue. At this point we still have two boxes in our JavaScript engine. See the following code:

var num = 2; function pow(num) { return num * num; } pow(num); setTimeout(callback, 10000); function callback(){ console.log('hello timer! '); }

We can complete our diagram by drawing it like this:

As you can see setTimeout runs in the browser context. After 10 seconds, the timer is fired and the callback function is ready to run. But first it has to go through the callback queue. A callback queue is a queue data structure that, as the name implies, is an ordered queue of functions.

Each asynchronous function must pass through the callback queue before it is put on the call stack. But who is driving this function? There is another component called the Event Loop.

The Event Loop now does only one thing: It should check that the call stack is empty. If there are some functions in the callback queue, and if the call stack is idle, then the callback should be sent to the call stack. Execute the function when it is finished.

Here’s a larger view of a JavaScript engine for handling asynchronous and synchronous code:

Imagine that callback() is ready to execute. When pow() completes, the call stack is empty and the event loop pushes the callback(). That’s it! Even though I’ve simplified things a little bit, if you understand the above diagram, then you can understand everything about JavaScript.

Remember that the Browser API, callback queues, and event loops are the backbone of asynchronous JavaScript.

If you like video, I recommend watching Philip Roberts’ video: What is the Event Loop. This is one of the best explanations for the time cycle.

youtube: https://www.youtube.com/embed…

Stick with it, because we haven’t used asynchronous JavaScript yet. ES6 Promises We’ll talk about ES6 Promises in more detail later on.

Callback Hell and ES6 Promise

Callbacks are everywhere in JavaScript. They are used for both synchronous and asynchronous code. For example, the map method:

function mapper(element){
    return element * 2;
}
[1, 2, 3, 4, 5].map(mapper);

The mapper is the callback function passed in the map. The above code is synchronous. But there is an interval to consider:

function runMeEvery(){ console.log('Ran! '); } setInterval(runMeEvery, 5000);

This code is asynchronous, and we pass the callback runMeEvery in the setInterval. Callbacks are common in JavaScript, so in recent years there has been a problem: callback hell.

Callback hell in JavaScript refers to the “style” of programming, where callbacks are nested within nested… . In callbacks of other callbacks. It is the asynchronous nature of JavaScript that causes programmers to fall into this trap.

To be honest, I’ve never come across an extreme callback pyramid, probably because I value code readability and always try to stick to it. If you find yourself in callback hell, you have too many functions.

I’m not going to discuss callbackhell here, but if you’re interested, there’s a site I recommend: Callbackhell.com explores this issue in more depth and offers some solutions. What we’re going to focus on now is ES6 Promise. ES6 Promise is a complement to the JavaScript language designed to solve the dreaded hell of callbacks. But what is a Promise?

JavaScript Promises are representations of future events. A Promise can end in success: In the jargon, it has resolved. But if the Promise fails, we say it is in the Rejected state. Promises also have a default state: Each new Promise starts with a pending state.

Create and use Promises

You create a new Promise by passing the callback function to the method of the Promise constructor to be called. The callback function can take two arguments: resolve and reject. Let’s create a new Promise that will resolve after 5 seconds (you can try these examples in the browser console) :

const myPromise = new Promise(function(resolve){
    setTimeout(function(){
        resolve()
    }, 5000)
});

As you can see, resolve is a function that we call to make the Promise successful. In the following example, reject returns the rejected Promise:

const myPromise = new Promise(function(resolve, reject){
    setTimeout(function(){
        reject()
    }, 5000)
});

Note that in the first example, you can omit reject because it is the second argument. But if you intend to use reject, you cannot omit resolve. In other words, the following code will not work and will end up with the RESOLVED Promise:

// Can't omit resolve !
const myPromise = new Promise(function(reject){
    setTimeout(function(){
        reject()
    }, 5000)
});

Promise doesn’t seem so useful right now. These examples do not print anything to the user. Let’s add some data. Resolved and Rejected Promises can both return data. Here’s an example:

const myPromise = new Promise(function(resolve) {
  resolve([{ name: "Chris" }]);
});

But we still don’t see any data. To extract data from the Promise, you also need a method called Then. It needs a callback (ironic!). To receive the actual data:

const myPromise = new Promise(function(resolve, reject) {
  resolve([{ name: "Chris" }]);
});
myPromise.then(function(data) {
    console.log(data);
});

As a JavaScript developer, you’ll mostly interact with Promises that come from the outside. Instead, library developers are more likely to wrap legacy code in a Promise constructor, as shown below:

const shinyNewUtil = new Promise(function(resolve, reject) {
  // do stuff and resolve
  // or reject
});

We can also create and resolve a Promise by calling promise.resolve () if needed:

Promise.resolve({ msg: 'Resolve! '}) .then(msg => console.log(msg));

So just to recap, JavaScript Promises are bookmarks for future events. Events start in a pending state and can be resolved or rejected. Promises can return data, which is extracted by appending a Then to the Promise. In the next section, we’ll see how to handle errors from Promises.

Error handling in ES6 Promise

Error handling in JavaScript has always been simple, at least for synchronized code. Take a look at the following example:

function makeAnError() { throw Error("Sorry mate!" ); } try { makeAnError(); } catch (error) { console.log("Catching the error! " + error); }

The output will be:

Catching the error! Error: Sorry mate!

The error is caught in the catch block. Now let’s try using an asynchronous function:

function makeAnError() { throw Error("Sorry mate!" ); } try { setTimeout(makeAnError, 5000); } catch (error) { console.log("Catching the error! " + error); }

Because of setTimeout, the above code is asynchronous. What happens if you run it?

throw Error("Sorry mate!" ); ^ Error: Sorry mate! at Timeout.makeAnError [as _onTimeout] (/home/valentino/Code/piccolo-javascript/async.js:2:9)

This time the output is different. The error did not pass the catch block. It can propagate freely on the stack.

That’s because try/catch only applies to synchronous code. If you’re curious, you can get a detailed explanation of the problem in Node.js’ error handling.

Fortunately, Promise has a way to handle asynchronous errors as if they were synchronous.

const myPromise = new Promise(function(resolve, reject) { reject('Errored, sorry! '); });

In the example above, we can use catch to handle the program error and take the callback again:

const myPromise = new Promise(function(resolve, reject) { reject('Errored, sorry! '); }); myPromise.catch(err => console.log(err));

We can also call promise.reject () to create and reject Promise:

Promise.reject({msg: 'Rejected! '}).catch(err => console.log(err));

ES6 Promise combinators: Promise. All, Promise. Allsettle, Promise. Any and their friends

Promise was not alone. The Promise API provides a number of methods for grouping promises together. One of the most useful is promise.all, which accepts a series of promises and returns a Promise. The problem is that if any Promise is rejected, Promise. All rejects.

Promise. Race resolves or rejects immediately after a Promise ends in the array. If one of the Promises rejects, it will still do so.

Newer versions of V8 will also implement two new combinators: Promise. Allsettled and Promise. Any. Promise. Any is still in the early stages of the proposal: At the time of this writing, it is not supported.

Promise. Any can indicate whether any promises are fullfilled. The difference with Promise. Race is that Promise. Any does not reject, even if one of the promises is rejected.

The most interesting is Promise. Allsettle. It still requires a series of Promises, but if one of them rejects, it will not be short-circuited. This is useful when you want to check that all of the Promise arrays have been resolved. You can think of it as always working against Promise. All.

ES6 Promise and MicroTask queues

If you remember from the previous section, every asynchronous callback function in JavaScript ends up in the callback queue before it is pushed on the call stack. But the callback functions passed in the Promise have a different fate: they are processed by the microtask queue, not by the callback queue.

One interesting thing you should notice is that the microtask queue takes precedence over the callback queue. Callbacks from the microtask queue take precedence when the event loop checks to see if any new callbacks are ready to be pushed on the call stack.

These mechanisms are covered in more detail by Jake Archibald in Tasks, Microtasks, Queues, and Timelines, which is a great article.

Asynchronous evolution: from Promise to async/await

JavaScript is evolving rapidly, and we continue to improve the language every year. The Promise seemed to be reaching the end, but a new syntax for ECMAScript 2017 (ES8) was born: async/await.

Async /await is just a stylistic improvement we call syntactic sugar. Async /await does not change JavaScript in any way (remember that JavaScript must be backward compatible with old browsers and should not break existing code).

It’s just a new way to write asynchronous code based on Promises. Let’s take an example. Before we used the Promise of then:

const myPromise = new Promise(function(resolve, reject) {
  resolve([{ name: "Chris" }]);
});
myPromise.then((data) => console.log(data))

Now that we are using async/await, we can look at handling asynchronous code in a synchronous manner in another way. We can wrap the Promise in a function labeled async and wait for the result:

const myPromise = new Promise(function(resolve, reject) {
  resolve([{ name: "Chris" }]);
});
async function getData() {
  const data = await myPromise;
  console.log(data);
}
getData();

Now the interesting thing is that asynchronous functions will always return promises, and no one can stop you from doing so:

async function getData() {
  const data = await myPromise;
  return data;
}
getData().then(data => console.log(data));

How do you handle errors? One benefit async/await provides is the opportunity to use try/catch. (See Exception handling and testing methods in asynchronous functions.) Let’s look at Promise again, where we use catch handlers to handle errors:

const myPromise = new Promise(function(resolve, reject) { reject('Errored, sorry! '); }); myPromise.catch(err => console.log(err));

Using asynchronous functions, we can refactor the following code:

async function getData() {
  try {
    const data = await myPromise;
    console.log(data);
    // or return the data with return data
  } catch (error) {
    console.log(error);
  }
}
getData();

Not everyone will use this style. Try /catch will mess up your code. There is another problem with try/catch, though. Look at the following code to raise an error in a try block:

async function getData() { try { if (true) { throw Error("Catch me if you can"); } } catch (err) { console.log(err.message); } } getData() .then(() => console.log("I will run no matter what!" )) .catch(() => console.log("Catching err"));

Which string is printed to the console? Remember that try/catch is a synchronous construct, but our asynchronous function produces a Promise. They were running on two different tracks, like two trains. But they will never meet! That is, an error thrown by a throw will never trigger the catch handler of getData(). Running the above code will lead to “Catch me if you can” and then “I’ll run anyway!” .

In fact, we don’t want the throw to trigger the current processing. One possible solution is to return promise.reject () from the function:

async function getData() { try { if (true) { return Promise.reject("Catch me if you can"); } } catch (err) { console.log(err.message); }}

The error will now be handled as expected:

getData() .then(() => console.log("I will NOT run no matter what!" )) .catch(() => console.log("Catching err")); "Catching err" // output

Beyond that async/await seems to be the best way to build asynchronous code in JavaScript. We have better control over error handling and the code looks cleaner.

I do not recommend refactoring all JavaScript code to async/await. This must be a choice after discussion with the team. But if you’re working on your own, whether you use simple Promise or async/await is a matter of personal preference.

conclusion

JavaScript is a scripting language for the Web that has the feature of being compiled and then interpreted by the engine. Among the most popular JavaScript engines are V8 for Google Chrome and Node.js, SpiderMonkey for the Web browser Firefox, and JavaScriptCore for Safari.

JavaScript engines are made up of many parts: call stacks, global memory, event loops, and callback queues. All of these parts work together in a perfectly tuned way to handle both synchronous and asynchronous code in JavaScript.

The JavaScript engine is single-threaded, which means there is only one call stack for running functions. This limitation is fundamental to the asynchronous nature of JavaScript: any actions that take time must be handled by an external entity (such as the browser) or by a callback function.

ECMAScript 2015 gives us Promises to simplify the asynchronous code flow. A Promise is an asynchronous object that represents the failure or success of an asynchronous operation. But the improvements did not stop there. In 2017 async/await was born: a stylistic addition to Promise that can be used to write asynchronous code as if it were synchronous.


This article first send WeChat messages public number: front-end pioneer

Welcome to scan the two-dimensional code to pay attention to the public number, every day to push you fresh front-end technology articles


Read on for the other great articles in this column:

  • 12 Amazing CSS Experiment Projects
  • 50 React Interview Questions You Must Know
  • What are the front-end interview questions at the world’s top companies
  • 11 of the best JavaScript dynamic effects libraries
  • CSS Flexbox Visualization Manual
  • React from a designer’s point of view
  • The holidays are boring? Write a little brain game in JavaScript!
  • How does CSS sticky positioning work
  • A step-by-step guide to implementing animations using HTML5 SVG
  • Programmer 30 years old before the monthly salary is less than 30K, which way to go
  • 14 of the best JavaScript data visualization libraries
  • 8 top VS Code extensions for the front end
  • A complete guide to Node.js multithreading
  • Convert HTML to PDF 4 solutions and implementation