JS Engine:
- Program that executes JS code. Ex. Google's V8.
- V8 powers Chrome, Node.js
- Other browsers have their own JS Engine.
JS Engine has 2 parts:
- Call Stack: Where our code is executed using execution context.
- Heap: Unstructured memory pool, used for storing objects.
Every c/r program needs to be converted to machine code. Done via two processes:
1. Compilation: Entire code is converted into machine code at once, written to a binary file that can be execued by a computer.
Source Code -(compiled)-> Binary Code[Portable File] -(executed)-> Pgm Run
Execution can happen way after compilation.
2. Interpretation: Interpreter runs through the source code, executes it line by line.
Source code -(execution line by line)-> Pgm Run
- Here code still needs to be converted to machine code.
- They are much slower as compared to compiled l/gs.
3. JIT i.e Just in Time Compilation:
- Modern JS Engine is a mix of compilation & interpretation to make it fast.
- Entire code is converted into machine code at once, then executed immediately. Source Code -(compiled)-> Machine code -(executed)-> Pgm Run
- There is no intermediate portable file to execute.
- Execution happens immediately after compilation.
- Hence, JS now is much faster than interpreted l/gs due to this technique.
Compilation Process in JS:
Step 1. Parsing:
- Our code is Parsed i.e read by JS engine into AST or Abstract Syntax Tree.
- Works by splitting code into a tree based on keywords like const, let, function etc which are meaningful to the l/g.
- Then saves the code into a tree in a structured way.
- Also check for any syntax errors.
- AST has nothing to do with the DOM tree. AST is just representation of our code inside the JS Engine.
Step 2, 3[combined]: Compilation + Execution
- AST is compiled and immediately executed after it, using JIT.
- Execution happens in Call stack.
- Modern JS has clever optimisation strategies, i.e they quickly create an unoptimized version of machine code in begining so as to start execution asap.
- In the background, this code is again recompiled during an already running pgm execution.
- Done in mutiple iterations, and after each optimisation the unoptimized code is swapped with newly optimized code without ever stopping the code execution. This makes V8 so fast.
- All this parsing, compilation, execution happens in some special thread inside JS Engine which we can't access using our code completely separate from the main thread which is running our code using call stack.
- JS is no more just interpreted l/g. It has JIT compilation which makes it way faster than interpreted l/gs.
JS Runtime = JS Engine + Web APIs + C/B Queue
- JS Runtime: Container including all the things that we need to use JS.
- Heart of any JS runtime is JS Engine.
- Without JS Engine, there is no runtime hence no JS at all.
- JS Engine alone is not enough, we need access to Web APIs like DOM, Fetch, Timers etc.
- Web APIs: Functionality provided to engine by runtime, but not part of JS engine. Ex. window object in browser, global object in node.
- Callback Queue is a data structure containing all the functions ready to be executed. Ex. click, timer, data etc
- DOM Event handler fns are also called as callback fns.
- When Call stack is empty, callback fn is shifted from C/B queue to Call stack for execution.
- The continuous checking & shifting is performed by Event Loop.
- Event loop is something which enables JS to have a non-blocking concurrency model.
- For node, we don't have Web APIs provided by browser. We have something called as C++ bindings & thread pool.
How JS Code executes on the Call Stack
- JS has single thread of execution, hence can do only thing at a time. Hence, no multiasking in JS.
- APIs are provided by environment, but not the part of language. Ex. Web APIs like Timers, Fetch, DOM, Geolocation etc.
- Callback Queue: Ready to be executed callback fns that are attached to some event which has occurred.
- Whenever call stack is empty, event loop transfers callback from calback to queue to call stack for execution.
- Event loop is hence an essential piece which makes Async behavior in JS possible.
- Concurrency Model: How a l/g handles multiple things happening at the same time.
- Essential parts of JS Runtime:
- Call Stack
- Web APIs
- Callback Queue
- Event loop
- Everything related to DOM is part of Web APIs, not JS.
- Image loading happens in async way, had it been in sync way then it would have been blocking i.e not on main thread rather Web APIs environment.
- All the event listeners, .then() etc work happens in WEb APIs environment and not on call stack.
- Callback fns are placed in callback queue waiting for them to be executed onn call stack.
- Callback queue is like a todo list which a call-stack has to complete.
- Duration mentioned is the minimum delay before for execution, and not the time for execution.
- Callback queue also contains callbacks coming from DOM events, clicks, keypress etc. DOM events are not async behavior, but they use callback queue for their execution.
- Event loop keeps checking the callback queue until its empty. Each callback placed on call stack is called as event loop tick.
- Event loop orchestrates the entire JS runtime.
- JS has itself no sense of time as Async code doesn't execute in engine. Its the runtime which manages async behavior and the event loop who decides which callback to be executed.
- Engine or Call stack simply executes the code given to it.
- When an image is to be loaded, the event listener keeps on waiting in the Web APIs environment until load event is fired off. When its fired, then only it goes to callback queue as a callback fn waiting for its turn to be executed on call stack.
Microtask Queue:
- Callbacks from Promises don't go to callback queue, they go to microtask queue.
- This queue has higher priority over callback queue.
- Event loop checks this queue first, executes all of its task first, then goes to callback queue for execution.
- Callbacks on promises are called microtasks, hence its called microtask queue. There are other microtasks also, but not relevant here as of now. Event loop decides when each callback is executed. It gives microtask higher priority as compared to callback queue
- Microtasks can cut inline before all other regular callback queues.
- Promise.resolve() : creates a promise which is resolved immediately, and its success value is passed inside it as argument. then() callback is called with resolved value as argument.
console.log("Direct simple console BEGIN");
setTimeout(() => console.log("Text from Timer"),0);
Promise.resolve("Text from Promise").then(res => console.log(res));
console.log("Direct simple console END");
Order of Output:
Direct simple console BEGIN
Direct simple console END
Text from Promise
undefined
Text from Timer
- A microtask queue can even starve the callback queue also if there are lot of microtasks in it or time-consuming microtasks.
console.log("Direct simple console BEGIN");
setTimeout(() => console.log("Text from Timer"),0);
Promise.resolve("Text from Promise1").then(res => console.log(res));
Promise.resolve("Text from Promise2").then(res => {
for(let i=0; i<5000; i++)
console.log(res);
});
console.log("Direct simple console END");