macro and micro tasks queue

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.

event loop

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);

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容

  • 概述: event loop(事件循环)是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技...
    印第安老斑鸠_333阅读 853评论 0 0
  • NodeJS 于其它任何平台的区别在于它如何处理 I/O。当我们听到 NodeJS 被一些人介绍的时候总是说:非阻...
    吃柠檬的刺猬阅读 478评论 0 1
  • 什么是事件循环(Event Loop) 事件循环能让 Node.js 执行非阻塞 I/O 操作,尽管JavaScr...
    假面猿阅读 783评论 0 0
  • 前言 Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种...
    CodeMT阅读 414评论 0 0
  • JS中比较让人头疼的问题之一要算异步事件了,比如我们经常要等后台返回数据后进行dom操作,又比如我们要设置一个定时...
    si_月阅读 1,015评论 0 0