The Event loop is only a watchdog that ensures that the Call Stack and Callback Queue are in constant communication. It first determines whether the call stack is free, and then informs the user of the callback queue. The callback function is then passed to the Call stack, which executes it. The call stack is out and the global execution context is free once all of the callback functions have been performed.
Microtask and Macrotask
We saw in the last section how JS Engine works. Coming to the task queue, we learned that it‘s’ where callbacks are enqueued and executed when the main thread is done with.
But, deep down the task queue, something else is going on. The tasks are broken down further into microtask and macrotask.
On one cycle of the event loop:
while (eventLoop.waitForTask()) {
eventLoop.processNextTask()
}
Exactly one macrotask is processed from the queue (a task queue is a macrotask queue). After this has finished, all the microtasks enqueued in the microtask queue are processed within the same cycle. These microtasks can enqueue other microtasks, which will be run until they are all exhausted.
while (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue()
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask()
}
const microtaskQueue = eventLoop.microTaskQueue
while (microtaskQueue.hasNextMicrotask()) {
microtaskQueue.processNextMicrotask()
}
}
This might take a long time before the next macrotask is run. This might lead to an unresponsive UI or idling in our application.
Note: Not all callbacks have same priority. Some callbacks use a special queue known as the micro task queue/Job queue, which has higher priority order than the callback queue. It introduces more interesting concepts like starvation in a queue.
Micro tasks
An event loop can have more than one task queues. Besides the (macro) task queue, an event loop also has a micro task queue. In its general execution, the event loop would pick up one task from the (macro) task queue and push it to the call stack for execution. In the next iteration it would pick another task from the (macro) task queue and repeat the same. However, this flow will change if there are tasks available on the micro task queue.
If there are tasks present on the micro task queue they would be processed first (in the current iteration of the event loop) till there are none left. After the micro task queue is exhausted the next (macro) task would be processed (in the next event loop iteration).
Some examples of macro tasks are - setTimeout, setInterval, setImmediate or user input. Some examples of micro tasks are - Promise and process.nextTick.
5. How does the event loop handle promises in JavaScript? Promises in JavaScript represent a future value. When a promise is resolved or rejected, its corresponding then/catch/finally callbacks are scheduled as a task in a special “microtask queue” queue. The event loop prioritizes this special microtask queue, processing all microtasks before moving on to other tasks. This ensures promises are handled swiftly and efficiently.
6. How can understanding the event loop improve my JavaScript code? Understanding the event loop can help you write more efficient, non-blocking code by optimizing the scheduling of tasks. It helps you understand the execution order of your code and handle asynchronous operations more effectively. Ultimately, it can lead to improved performance, smoother user experiences, and better handling of resources in your JavaScript applications.
A noteworthy feature of the event loop is its ability to pause execution when necessary, freeing up resources for other operations. This proves invaluable when interacting with high-latency services such as databases and networks.
Macro-tasks within an event loop: Macro-task represents some discrete and independent work. These are always the execution of thee JavaScript code and micro-task queue is empty. Macro-task queue is often considered the same as the task queue or the event queue. However, the macro-task queue works the same as the task queue. The only small difference between the two is that the task queue is used for synchronous statements whereas the macro-task queue is used for asynchronous statements.
In JavaScript, no code is allowed to execute until an event has occurred. {It is worth mentioning that the execution of a JavaScript code execution is itself a macro-task.} The event is queued as a macro-task. When a (macro) task, present in the macro-task queue is being executed, new events may be registered and in turn created and added to the queue.
Up on initialization, the JavaScript engine first pulls off the first task in the macro-task queue and executes the callback handler. The JavaScript engine then sends these asynchronous functions to the API module, and the module pushes them to the macro-task queue at the right time. Once inside the macro-task queue, each macro-task is required to wait for next round of event loop. In this way, the code is executed.
All micro-tasks logged are processed in one fell swoop in a single macro-task execution cycle. In comparison, the macro-task queue has a lower priority. Macro-tasks include parsing HTML, generating DOM, executing main thread JavaScript code and other events such as page loading, input, network events, timer events, etc.
Examples: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI Rendering
Micro-tasks within an event loop: A micro-task is said to be a function which is executed after the function or program which created it exits and only if the JavaScript execution stack is empty, but before returning control to the event loop being used by the user agent to drive the script’s execution environment. A Micro-task is also capable of en-queuing other micro-tasks.
Micro-tasks are often scheduled for things that are required to be completed immediately after the execution of the current script. On completion of one macro-task, the event loop moves on to the micro-task queue. The event loop does not move to the next task outside of the micro-task queue until the all the tasks inside the micro-task queue are completed. This implies that the micro-task queue has a higher priority.
Once all the tasks inside the micro-task queue are finished, only then does the event loop shifts back to the macro-task queue. The primary reason for prioritizing the micro-task queue is to improve the user experience. The micro-task queue is processed after callbacks given that any other JavaScript is not under mid-execution. Micro-tasks include mutation observer callbacks as well as promise callbacks.
In such a case wherein new micro-tasks are being added to the queue, these additional micro-tasks are added at the end of the micro-queue and these are also processed. This is because the event loop will keeps on calling micro-tasks until there are no more micro-tasks left in the queue, even if new tasks keep getting added. Another important reason for using micro-tasks is to ensure consistent ordering of tasks as well as simultaneously reducing the risk of delays caused by users.
Syntax: Adding micro-tasks:
queueMicrotask(() => {
// Code to be run inside the micro-task
});
The micro-task function itself takes no parameters, and does not return a value.
Examples: process.nextTick, Promises, queueMicrotask, MutationObserver
Event Loop
The event loop is an endless loop that is always running in the background on the JS engine. The general algorithm of an event loop is:
1.While there are tasks
-Execute the task, in an oldest first order
2.Sleep until a task appears, then go to 1
Tasks are jobs assigned to the JS engine. For Example:
1.When an external script loads, the task is to execute it.
2.When a user moves the mouse, the task is to dispatch mousemove event and execute it.
3.When the time is due for a scheduled setTimeout, the task is to run its callback.
The JavaScript engine does nothing most of the time, it only runs if a script/handler/event activates, then waits for more tasks (while sleeping and consuming close to zero CPU).
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
Tasks are scheduled so the browser can get from its internals into JavaScript/DOM land and ensures these actions happen sequentially. Between tasks, the browser may render updates. Getting from a mouse click to an event callback requires scheduling a task, as does parsing HTML, and in the above example, setTimeout.
Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task. The microtask queue is processed after callbacks as long as no other JavaScript is mid-execution, and at the end of each task. Any additional microtasks queued during microtasks are added to the end of the queue and also processed. Microtasks include mutation observer callbacks, and as in the above example, promise callbacks.
Once a promise settles, or if it has already settled, it queues a microtask for its reactionary callbacks. This ensures promise callbacks are async even if the promise has already settled. So calling .then(yey, nay) against a settled promise immediately queues a microtask. This is why promise1 and promise2 are logged after script end, as the currently running script must finish before microtasks are handled. promise1 and promise2 are logged before setTimeout, as microtasks always happen before the next task.
The important points are:
-Tasks are taken from the Task Queue.
-Task from the Task Queue is a Macrotask != a Microtask.
-Microtasks are processed when the current task ends and the microtask queue is cleared before the next macrotask cycle.
-Microtasks can enqueue other microtasks. All are executed before the next task inline.
-UI rendering is run after all microtasks execution.
To demonstrate that microtasks are run before any macrotask, let’s look at this example:
console.log('script start');
setTimeout(function () { console.log('setTimeout');}, 0);
Promise.resolve().then(function () { console.log('promise1'); })
.then(function () { console.log('promise2'); });
console.log('script end');
script start
script end
promise1
promise2
setTimeout
https://blog.bitsrc.io/microtask-and-macrotask-a-hands-on-approach-5d77050e2168
OK, we are getting somewhere. Now, let’s test our example(example.js) we used earlier to demonstrate micro/macrotask but we a little modification:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
setMicro(()=> {
console.log('micro1')
setMicro(()=> {
console.log('micro2')
})
})
console.log('script end');
The runScript registered our code as a macrotask, and on exit, its macrotask callback runs our code which logs script start, setTimeout sets a macrotask and micro1 setMicro sets a microtask. script end is logged last. After each macrotask execution, all microtasks in the microtask queue are all processed. micro1 callback runs which logs micro1 and also, set another microtask micro2. On exit of the micro1 microtask, the micro2 microtask is run which logs micro2. On exit again, no other microtask is queued so another macrotask is run. setTimeout is run which logs setTimeout. As there are no more macrotask enqueued the for-loop exits and our custom JS engine yields.
To run it in our custom JS engine, we translate:
// js_engine.js
...
js_stack.push(`console.log('script start');`)
js_stack.push(`setTimeout(function() {
console.log('setTimeout');
}, 0);`)
js_stack.push(`setMicro(()=> {
console.log('micro1')
setMicro(()=> {
console.log('micro2')
})
})`)
js_stack.push(`console.log('script end');`)
...
You may argue that setTimeout should be logged first because a macrotask is run first before clearing the microtask queue. And, when looking at the script, there is no macrotask enqueued before the setTimeout call.
Well, you are right. But, no code runs in JS unless an event has occurred. The event is queued as a macrotask.
At the execution of any JS file, the JS engine wraps the contents in a function and associates the function with an event either start or launch. The JS engine emits the start event, the events are added to the task queue (as a macrotask).
On initialization the JS engine first pulls off the first task in the macrotask queue and executes the callback handler. Thus, our code is run.
.1) takes the contents of the input file, 2) wraps it in a function, 3) associates that function as an event handler that is associated with the “start” or “launch” event of the program; 4) performs other initialization, 5) emits the program start event; 6) the event gets added to the event queue; 7) the Javascript engine pulls that event off the queue and executes the registered handler, and then (finally) 8) our program runs! — “Asynchronous Programming in Javascript CSCI 5828: Foundations of Software Engineering Lectures 18–10/20/2016” by Kenneth M. Anderson
So we see the script running is the first macrotask queued. The callback runs our code. Following through, script start is printed by the console.log call. Next, the setTimeout function is called which queues a macrotask with the handler. Then, the Promise call queues a microtask, then the console.log prints script end. The initial callback then exits.
As it is a macrotask, the microtasks are processed. The Promise callback is run which logs promise1, it returns and queues another microtask through its then() function. It is processed (Remember, microtasks can queue extra microtasks in one cycle, yet all are processes before yielding control to the next macrotask cycle) which prints promise2. No other microtasks are queued and the microtask queue is empty. The initial macrotask is cleared, remaining the macrotask by the setTimeout function.
At this point, the UI rendering function is run (if any). The next macrotask is processed which is the setTimeout macrotask. It logs setTimeout and is cleared from the queue. As there are no more tasks and the stack is also empty the JS engine yields.
// js_engine.js
1.➥ let macrotask = []
2.➥ let microtask = []
3.➥ let js_stack = []
// microtask
4.➥ function setMicro(fn) {
microtask.push(fn)
}
// macrotask
5.➥ function setMacro(fn) {
macrotask.push(fn)
}
// macrotask
6.➥ function runScript(fn) {
macrotask.push(fn)
}
7.➥ global.setTimeout = function setTimeout(fn, milli) {
macrotask.push(fn)
}
// your script here8.➥ function runScriptHandler() {
8I.➥for (var index = 0; index < js_stack.length; index++) {
8II.➥eval(js_stack[index])
}
}
// start the script execution
9.➥runScript(runScriptHandler)
// run macrotask
10.➥for (let ii = 0; ii < macrotask.length; ii++) {
11.➥ eval(macrotask[ii])()
if (microtask.length != 0) {
// process microtasks
12.➥ for (let __i = 0; __i < microtask.length; __i++) {
eval(microtask[__i])()
}
// empty microtask
microtask = []
}
}
We have the runScript function, this function emulates the global “start” event of the JS engine during initialization. Since the global event is a macrotask thingy, we push the fn callback to the macrotask queue. The runScript fn parameter (8.) encapsulates the code in the js_stack (ie the code in our JS file), so when run, the fn callback bootstraps the code in the js_stack.
First, we execute the runScript function, which as we have learned, runs the entire code in the js_stack. As stated earlier, after the stack is cleared and empty. The task queue(macrotask) is run(10.). For each cycle of macrotask execution(11.), the entire microtask callbacks are processed(12.).
We for-looped through the macrotask array, and executed the current function in the index. Still inside the loop and index, we for-looped through the microtask array and execute all. Though, some microtasks can enqueue more microtasks. The for-loop cycles through them all until they are all exhausted. hen, it empties the microtask array. Then, the next macrotask is processed.
example2:
// Let's listen for attribute changes on the// outer element
new MutationObserver(function () { console.log('mutate');}).observe(outer, { attributes: true,});
// Here's a click listener…
function onClick() {
console.log('click');
setTimeout(function () { console.log('timeout'); }, 0);
Promise.resolve().then(function () { console.log('promise'); });
outer.setAttribute('data-random', Math.random());
}
// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
click
promise
mutate
click
promise
mutate
timeout
timeout
Running sequence of long or repeated tasks
So here is a performance tip, if you have a big sync calculation to do and you can’t use a web worker, consider splitting the task into multiple tasks using setTimeout. That way browser will not become unresponsive!
In an event loop iteration, micro tasks are processed till the micro task queue is empty. Since a micro task can create other micro task(s) a long sequence of micro tasks can block the UI. This is quite different from a sequence of macro tasks since they’re processed in different iterations of the event loop, which gives browser a chance to paint the updated DOM or respond to user input.
The following function will be called recursively infinitely but the UI will remain responsive. You may try this in your browser console if you wish.
function runMacroTask(){
console.log('Macro task running');
setTimeout(runMacroTask,0);// recursively calling the function
}
runMacroTask();
However, if we replace the above with a micro task, the UI (and execution thread) will be blocked completely. Careful when pasting this in your browser console as it will make the tab unresponsive.
function runMicroTask(){
console.log('Micro task running');
Promise.resolve().then(runMicroTask);// recursively calling the function
}
runMicroTask();
When to use micro tasks
Generally I would recommend using macro tasks for a long sequence of tasks (avoid using process.nextTick in Node for the same). Use micro tasks only if the tasks themselves don’t create other micro tasks as it would block execution.
Another way to run tasks repeatedly is to use requestAnimationFrame in browsers. Its a great way to execute tasks related to UI. The callback passed to requestAnimationFrame is executed only before the browser does the next paint. This can help avoid unnecessary calls since the number of invocations will match the display refresh rate and therefore the function will be called less often than a recursive setTimeout.
reference: https://segmentfault.com/a/1190000016278115
事件循环机制:执行一个宏任务就清空一次微任务队列,然后再执行宏任务
1.执行全局script,这些代码有一些是同步语句,有些是异步语句(如setTimeout等)分配到microtask/macrotask queue;
macrotask,也叫tasks,异步回调会依次进入macrotask queue等待后续被调用:
setTimeout/setInterval
UI rendering (浏览器独有)
requestAnimationFrame (浏览器独有)
I/O
setImmediate (Node独有)
microtask,也叫jobs,异步回调会依次进入micro task queue,等待后续被调用:
Promise.then()
Object.observe
MutationObserver
process.nextTick (Node独有)
2.全局script代码执行完毕后,stack会清空;
3.从microtask queue中取出位于队首的回调任务放入Stack中执行,执行完后microtask queue长度减1;直到把microtask queue中的所有任务都执行完毕。(注意,如果在执行microtask的过程中又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行)微任务队列中所有的任务都会被依次取出来执行,直到microtask queue为空;microtask queue中的所有任务都执行完毕,此时microtask queue为空,调用栈stack也为空;
4. next loop:取出macrotask queue中位于队首的任务,放入Stack中执行;执行完毕后,调用栈stack为空;macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
5.重复第3-4;
图中没有画UI rendering的节点,因为这是由浏览器自行判断决定的,但是只要执行UI rendering,它的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render。
使用Promise.resolve(value)等方法的时候,如果promise对象立刻就能进入resolve状态的话,那么.then里面callback是同步调用?.then中callback是异步进行的。
var promise= new Promise(resolve=>{
console.log("inner promise");// 1
resolve(42);
});
promise.then(value=>{
console.log(value);// 3
});
console.log("outer promise");// 2
执行顺序:
inner promise // 1
outer promise // 2
42 // 3
JavaScript代码会按照文件从上到下的顺序执行,所以最开始<1>会执行,然后resolve(42);被执行。这时promise对象的已经变为确定状态,FulFilled设置为42。
下面的代码promise.then注册了<3>这个回调函数,即使在调用promise.then注册回调函数的时候promise对象已经是确定的状态,Promise也会以异步的方式调用该回调函数,这是在Promise设计上的规定方针。
因此<2>会最先被调用,最后才会调用回调函数<3>。
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {console.log('async2');}
console.log('script start');
setTimeout(function() {console.log('setTimeout1');}, 200);
setTimeout(function() {
console.log('setTimeout2');
new Promise(function(resolve) {resolve();})
.then(function() {console.log('then1')})
new Promise(function(resolve) {
console.log('Promise1');
resolve();
}).then(function() {console.log('then2')})
},0)
async1();
new Promise(function(resolve) {
console.log('promise2');
resolve();
}).then(function() {console.log('then3');});
console.log('script end');
output:
script start'
async1 start
async2
promise2
script end
async1 end
then3
setTimeout2
Promise1
then1
then2
setTimeout1
setTimeout(() => console.log('a'));
console.log('i')
Promise.resolve()
.then(() => console.log('b'))
.then(() => {
Promise.resolve('c')
.then((data) => {
setTimeout(() => console.log('d')); //挂起,放到宏任务队列之后
console.log('f'); //执行,输出'f'
return data; //该函数返回值是 'c'})
}).then(data => console.log(data)); // 接收到的就是输出 'c'
setTimeout(() => console.log('e'));
console.log('j')
setTimeout(() => console.log('f'));
})
setTimeout(() => console.log('g'));
console.log('h')
output:
i
h
b
j
f
c
a
g
e
f
d
new Promise((resolve) => {
console.log('1')
resolve()
console.log('2')
}).then(() => {console.log('3')})
setTimeout(() => console.log('4'))
console.log('5')
由于javascript是单线程任务所以主线程只能同时处理一个任务,所以把异步的事件放在同步的事件处理完成之后再来依次处理。
同步:console.log(1)->console.log(2)->console.log(5);
异步:(微任务)console.log(3)->(宏任务)console.log(4);