- Gaurav Pandvia
- Original link: medium.com/gaurav.pan…
- Ladders may be required for some links in this article.
- Criticisms and corrections are welcome.
Today, Web developers (we prefer to be called front-end engineers) can do everything from providing interaction in a browser to developing computer games, desktop tools, cross-platform mobile applications, and even deploying on the server (like the most popular Node.js) to connect to any database with a single scripting language. Therefore, it is important to understand the internals of Javascript so that you can use it optimally and efficiently. This is also the main idea of this article.
The Javascript ecosystem is getting more complex. To build a modern Web application, it is inevitable to use Webpack, Babel, ESLint, Mocha, Karma, Grunt… Which one should I use? What’s all this for? I found this cartoon that perfectly captures the plight of web developers today:
Before diving into the sea of frameworks and libraries, every Javascript developer needs to understand how Javascript is implemented at the bottom. Almost every JS developer has heard the term “V8”, but some of them probably have no idea what it actually means or why it’s used. During the first year of my career, I learned very little about these buzzwords and was more concerned with getting the job done first. But that doesn’t satisfy my curiosity about how Javascript is fucking able to do all this. I decided to dig a little deeper. I scoured Google and found some excellent blogs, including Philip Roberts’ A Great Talk at JSConf on the Event Loop. So I decided to sum up my learning experience and share it. Since there is so much to learn, I have divided this article into two parts. This section introduces common terms, and the second section explains how they relate to each other.
Javascript is a single-threaded, single-concurrent language, which means it can only handle one task and execute one piece of code at a time. Its call stack, along with heap and queue, forms the Javascript concurrency model (implemented in V8). Let’s look at these words one by one.
- Call Stack: This is the data structure that records the functions we Call in a program. If we call a function to execute, we are pushing some record to the top of the call stack; When we return from a function, records pop up from the top of the call stack.
When we run the code in the figure above, we first look for the main function, the beginning of all execution. In the above example, the sequence of executions starts at console.log(bar(6)), so this time the execution is pushed onto the call stack. One layer above it is the function bar and its parameters, which in turn calls function foo, which is pushed onto the stack. Foo then returns a value, so it gets knocked off the call stack; Similarly, the bar pops up later, and finally the Console statement prints the result and pops up. All this took place in a few seconds.
You’ve all seen the long red error stack on the browser console, which in a top-down stack simply indicates the current state of the call stack and where errors are reported in the function (see figure below).
Sometimes we get stuck in an infinite loop when we recursively call a function multiple times, but Chrome limits the size of the call stack to 16,000 layers, which terminates the program and throws a stack limit error (see figure below).
- Heaps: Objects are allocated to heaps — loose structures in memory. All memory allocation for variables and objects is done in the heap.
- Queue: A Javascript runtime that contains a message queue, which is a sequence of messages to be processed and related callback functions to be executed. When there is enough room in the call stack, a message is pulled from the queue and processed, calling the associated function (and thus generating an initialization stack layer). When the stack empties again, message processing ends. In simple terms, these messages are queued, specifying callback functions to respond to external asynchronous events (such as mouse clicks or responses to HTTP requests). For example, when a user clicks a button without a corresponding callback function, no message is placed in the queue.
Event loop
When evaluating the performance of JS code, remember that calling functions on the stack makes the program fast or slow, console.log() is fast, but iterating for or while thousands of times is slow, and keeps the call stack occupied and blocked. And that’s called a blocking script, which you probably see in Webpage Speed Insights.
Network requests are slow, image requests are slow, but fortunately, service requests can be done with asynchronous functions like AJAX. What if those network requests were done using synchronization functions? Network request sent to the server, the server is just the sort of machines is somewhere, now let’s assume that the server returns a response may be slow, at this point, if I click on some of the CTA (call – to – the action button, or some other rendering needs to be done, it won’t have any reaction, because the web request prior to the call stack was blocked. In multithreaded languages such as Ruby, this is manageable, but in single-threaded languages such as Javascript, it is blocked until the function in the call stack returns a value. The browser doesn’t respond and the page crashes. We can’t provide a smooth user interface for the end user. So what do we do?
“Concurrency in JS – do one thing at a time, except for asynchronous callbacks”
The earliest solution was to use asynchronous callbacks, which meant that we added a callback to some part of the code that would be executed after the code was finished executing. We’ve all encountered asynchronous callbacks like $.get() for AJAX requests, setTimeout(), setInterval(), Promises. Nodes are all executed based on asynchronous functions. All those asynchronous callbacks don’t run immediately like synchronous functions like console.log() do, but at a later point in time, so they’re not immediately pushed onto the call stack. So where the hell did they go? How do you control them?
For example, if a web request is running in Javascript:
1. The request function is executed, passing an anonymous function to the 'onReadyStatechange' event as a callback to be executed in the future when the response is ready. "Script call done!" Output to the console immediately. 3. At some subsequent point, the response is returned, the callback is executed, and the response body is printed to the console.Copy the code
Decoupling calls to the response allow the Javascript runtime to do something else while waiting for the asynchronous operation to complete and undoing the callback execution. The browser comes in and calls its API, which is implemented in C++ to create threads to control asynchronous events such as DOM events, HTTP requests, setTimeout, and so on.
Those Web interfaces can’t push executing code onto the call stack by themselves, and if they can, the interfaces will appear randomly in your code (in an unmanageable order of execution). The message callback queue discussed above illustrates this. Any Web interface will push the callback to this queue when it finishes executing. The event loop is now responsible for controlling the execution of callbacks in the queue and pushing them onto the stack when they are empty. The basic job of the event loop is to listen to the call stack and the task queue, and when it sees that the stack is empty, it pushes the first task in the queue onto the stack. Each message or callback is processed after the previous task is processed.
while (queue.waitForMessage()) {
queue.processNextMessage();
}
Copy the code
In a Web browser, once an event occurs and an event listener is bound, the message is immediately added to the queue. If there is no listener, it means that the event is lost. So clicking on a click-event handler will add a message, as will any other event. The call to its callback will be the initial layer in the call stack, and since Javascript is single-threaded, polling and processing of subsequent messages is suspended until all calls in the call stack have returned. Subsequent (synchronous) function calls add a new call layer to the call stack.
In the next section, I’ll use an animation to show the code execution of the above process, explaining in depth what the different types of asynchronous functions are, who executes first in the queue, and tricks for features like zero latency.