— This article is taken from my official account “Sun Wukong, Don’t talk Nonsense”
JavaScript works in a single thread
One of the hallmarks of the JavaScript language is single threading. In other words, you can only do one thing at a time. From a JavaScript engine’s perspective, only one piece of code can be executing at any one time.
So why doesn’t JavaScript use multiple threads? After all, multithreading is more efficient.
The fact that JavaScript is single threaded has to do with what it’s used for. As a browser scripting language, JavaScript’s primary purpose is to interact with users and manipulate the DOM. This means that it has to be single-threaded, which can cause complex synchronization problems. If you’ve used multithreaded programming in the JAVA language, you know that while multithreading provides convenience, it also brings problems: performance overhead associated with thread switching, thread deadlocks, and so on.
Thus, to avoid complexity, JavaScript has been single-threaded since its inception, and this has been a core feature of the language and will not change.
What does a single thread mean? This means that once the program is started, there is only one call stack in the JavaScript engine, and only one piece of code can be executed at any one time.
JavaScript engine and JavaScript execution environment
The JavaScript engine’s job is to parse and execute the corresponding JS statements in strict compliance with the ECMAScript specification. That’s all.
Then there’s the JavaScript execution environment. If the host environment is a browser, then the JavaScript execution environment includes many things: JavaScript engines, WEB apis, Event loops, and more. (Host environment is Node environment is similar)
As you can see, in the host environment, the JavaScript execution environment contains the JavaScript engine. The two are inclusive, not equal. This has to be clear.
The Event Loop mechanism
We see that the Event Loop is included in the JavaScript execution environment. So what does this Event Loop do? Simply put, Event Loop is an implementation of asynchronous invocation in a JavaScript execution environment.
Let’s take a look at the definition and specification of Event Loop on the WHATWG website. Someone has turned these specifications into pseudocode, as follows:
eventLoop = { taskQueues: { events: [], // UI events from native GUI framework parser: [], // HTML parser callbacks: [], / /setTimeout, requestIdleTask
resources: [], // image loading
domManipulation[]
},
microtaskQueue: [
],
nextTask: function() {
// Spec says:
// "Select the oldest task on one of the event loop's task queues"
// Which gives browser implementers lots of freedom
// Queues can have different priorities, etc.
for (let q of taskQueues)
if (q.length > 0)
return q.shift();
return null;
},
executeMicrotasks: function() {
if (scriptExecuting)
return;
let microtasks = this.microtaskQueue;
this.microtaskQueue = [];
for (let t of microtasks)
t.execute();
},
needsRendering: function() {
return vSyncTime() && (needsDomRerender() || hasEventLoopEventsToDispatch());
},
render: function() {
dispatchPendingUIEvents();
resizeSteps();
scrollSteps();
mediaQuerySteps();
cssAnimationSteps();
fullscreenRenderingSteps();
animationFrameCallbackSteps();
while (resizeObserverSteps()) {
updateStyle();
updateLayout();
}
intersectionObserverObserves();
paint();
}
}
// how it work:
while(true) {
task = eventLoop.nextTask();
if (task) {
task.execute();
}
eventLoop.executeMicrotasks();
if (eventLoop.needsRendering())
eventLoop.render();
}
Copy the code
The above code roughly describes the composition and usage of the Event Loop. There are two concepts: taskQueues and Microtaskqueues. TaskQueues are commonly known as macro tasks, which are sets that separate tasks into queues (with different execution priorities, of course). As you can see, there are UI event queues, callback function queues, and so on. A microtaskQueue is often referred to as a microtask, which is structured as a queue.
How does the Event Loop work? Look at the final code:
while(true) {
task = eventLoop.nextTask();
if (task) {
task.execute();
}
eventLoop.executeMicrotasks();
if (eventLoop.needsRendering())
eventLoop.render();
}
Copy the code
The Event Loop is always running. In a loop, find a task from the macro taskQueues to execute. After each macro task is completed, all microtasks in the microtaskQueue are executed. The details are shown in the figure below:
So, you might ask: it’s not clear in the pseudocode how these macro and micro tasks are placed in the queue corresponding to the Event Loop.
No hurry. Let’s move on.
Conceptual integration
Host environment -> JavaScript engine
When the host (browser or Node environment) gets a piece of JavaScript code, the first thing it does is pass it to the JavaScript engine and ask it to execute it.
So, first of all, we should form a sensible understanding that a JavaScript engine will live in memory, waiting for a host to pass JavaScript code or functions to it to execute.
Host environment -> Event Loop -> JavaScript engine
However, implementing JavaScript is not a one-shot deal.
When the host environment encounters some events, it will put the events into the queue corresponding to the Event Loop and set the corresponding Watcher. When polling, Event Loop will ask whether the Event associated with the corresponding Watcher is complete. If so, it will pass the corresponding callback function or code (if any) to the JavaScript engine for execution.
In addition, we may also provide a WEB API to the JavaScript engine, such as setTimeout API, which the JavaScript engine will put into the queue corresponding to the Event Loop and set the corresponding timer Watcher. Based on a similar mechanism, the Event Loop will ask the timer Watcher whether the corresponding time is up. If the time is up, the callback for monitoring setTimeout is passed to the JavaScript engine for execution.
In the above procedure, the Event Loop actually uses the Watcher observer. Question from the previous chapter: How do these macro and micro tasks fit into the queue corresponding to the Event Loop? The answer is Watcher Watcher. The detailed process of its implementation is not covered here.
Host environment -> Event Loop -> JavaScript Engine -> Microtasks
In ES3 and earlier versions, the JavaScript engine itself does not yet have the ability to execute code asynchronously. This means that the host environment (via Event Loop) passes a piece of code to the JavaScript engine, and the engine executes the code directly in sequence, which is the host-initiated task.
However, after ES5, JavaScript introduced Promise. This way, the JavaScript engine itself can initiate tasks without the need for the browser to arrange them.
Adopting JSC engine terminology, we call host-initiated tasks macro tasks and JavaScript engine-initiated tasks micro tasks. Therefore, at this time, the Event Loop microtask queue is officially used.
So we know that before promises, all asynchronous events and implementations of the asynchronous Web API were macro tasks in the Event Loop.
conclusion
If we can understand these concepts, then we can solve the Event Loop related knowledge.
So let’s do a quick test. What is the output of the following code?
var r = new Promise(function(resolve, reject){
console.log("a");
resolve()
});
setTimeout(()=>console.log("d"), 0)
r.then(() => console.log("c"));
console.log("b")
Copy the code
JavaScript in-depth series:
“var a=1;” What’s going on in JS?
Why does 24. ToString report an error?
Here’s everything you need to know about “JavaScript scope.
There are a lot of things you don’t know about “this” in JS
JavaScript is an object-oriented language. Who is for it and who is against it?
Deep and shallow copy in JavaScript
JavaScript and the Event Loop
From Iterator to Async/Await
Explore the detailed implementation of JavaScript Promises