One job done (Js asynchronism, event loop and message queue, micro task and macro task)

preface

We all know that javascript is a single threaded, asynchronous, non blocking, parsing type scripting language.
  • Single thread??
  • Asynchronous??
  • Non blocking??
  • Then there are event loops, message queues, micro tasks and macro tasks

In recent days, I have read a lot of articles of big men in forums such as nuggets and Zhihu. It seems that I have learned a little. Then I summarize my experience here to help you understand it quickly and increase your memory.

 

Single thread and multi thread

JavaScript is designed to handle the interaction of browser web pages (DOM operation processing, UI animation, etc.), which determines that it is a single threaded language. If there are multiple threads operating the DOM at the same time, the web page will be a mess.

From this, we can know that js processing tasks are processed one by one, executed from top to bottom

console.log('start')
console.log('middle')
console.log('finish')

// start
// middle
// finish

At this time, students with pioneering thinking may say that if the processing of a task takes a long time (or waits), such as network requests, timers, waiting for mouse clicks, the subsequent tasks will be blocked, that is, all user interactions (buttons, scroll bars, etc.) will be blocked, which will bring a very unfriendly experience.

However:

console.log('start')
console.log('middle')
setTimeout(() => {
  console.log('timer over')
}, 1000)
console.log('finish')

// start
// middle
// finish
// timer over

 

You will find that timer over will print after printing, that is, the timer does not block the following code

In fact, JavaScript single thread means that there is only one thread in the browser responsible for interpreting and executing JavaScript code, that is, the JS engine thread, but the rendering process of the browser provides multiple threads, as follows:

  • JS engine thread
  • Event trigger thread
  • Timed trigger thread
  • Asynchronous http request thread
  • GUI rendering thread

When timer, DOM event listening or network request tasks are encountered, the JS engine will directly hand them over to webapi, that is, the corresponding thread provided by the browser (such as timer thread timing for setTimeout and asynchronous http request thread processing network requests) to process them, while the JS engine thread continues with other subsequent tasks, thus realizing asynchronous non blocking.

The timer triggering thread is only timed for setTimeout(..., 1000). Once the time comes, it will also give its corresponding callback function to the task queue for maintenance. The JS engine thread will go to the task queue to take out the task and execute it at an appropriate time.

When does the JS engine thread handle it? What is a message queue?

Event loop and message queue

JavaScript solves this problem through the mechanism of event loop.

In fact, the event cycle mechanism and task queue maintenance are controlled by the event trigger thread.

The event triggering thread is also provided by the browser rendering engine. It will maintain a task queue.

JS engine thread encounters asynchrony (DOM event listening, network request, setTimeout timer, etc.), It will give the corresponding thread to maintain the asynchronous task separately, wait for an opportunity (the timer ends, the network request succeeds, and the user clicks DOM), and then the event triggered thread will add the asynchronous corresponding callback function to the message queue, and the callback function in the message queue will be executed.

At the same time, the JS engine thread will maintain an execution stack, the synchronous code will be added to the execution stack and then executed, and the execution stack will exit at the end.

If the task in the execution stack is completed, that is, when the execution stack is empty (that is, the JS engine thread is idle), the event triggering thread will take a task from the message queue (that is, an asynchronous callback function) and put it into the execution stack for execution.

Message queues are queue like data structures that follow the first in first out (FIFO) rule.
1. All synchronization tasks are executed on the main thread to form an execution stack( execution context stack). 
2. In addition to the main thread, there is a"Task queue"(task queue). As long as the asynchronous task has the running result, the"Task queue"Place an event in.
3. One but"Execution stack"After all synchronization tasks in are completed, the system will read"Task queue",Look at the events inside. The corresponding asynchronous tasks end the wait state, enter the execution stack, and start execution.
4. The main thread repeats the third step above.

As long as the main thread is empty, it will be read"Task queue",This is JavaScript Operation mechanism of. This process is repeated. This mechanism is called event loop( event loop)Mechanism.

As mentioned above, there are synchronous code and asynchronous code in JavaScript., One is synchronous, the other is asynchronous. Synchronization tasks refer to tasks queued for execution on the main thread. The next task can only be executed after the previous task has been executed; Asynchronous tasks refer to tasks that enter the "task queue" instead of the main thread. Only when the "task queue" notifies the main thread that an asynchronous task can be executed will the task enter the main thread for execution.

Specifically, the operating mechanism of asynchronous execution is as follows. (the same is true for synchronous execution, because it can be regarded as asynchronous execution without asynchronous tasks.)

Synchronization:

console.log('hello 0')

console.log('hello 1')

console.log('hello 2')

// hello 0
// hello 1
// hello 2

They will be executed in turn, and the results (print results) will be returned after execution.

Asynchronous:

setTimeout(() => {
  console.log('hello 0')
}, 1000)

console.log('hello 1')

// hello 1
// hello 0

The above setTimeout function does not immediately return results, but initiates an asynchronous. SetTimeout is the asynchronous initiating function or registration function, () = > {...} Is an asynchronous callback function.

Asynchronous is generally the following:

  • Network request
  • timer
  • DOM time listening

Macro task and micro task

Promise is also used to handle asynchrony:

console.log('script start')

setTimeout(function() {
    console.log('timer over')
}, 0)

Promise.resolve().then(function() {
    console.log('promise1')
}).then(function() {
    console.log('promise2')
})

console.log('script end')

// script start
// script end
// promise1
// promise2
// timer over

"promise 1" "promise 2" printed before "timer over"?

Here is a new concept: macro task and micro task.

All tasks are divided into macro task and micro task:

  • Macro task: main code block, setTimeout, setInterval, etc. (you can see that each event in the event queue is a macro task, which is now called macro task queue)
  • Micro task: Promise, process Nexttick et al

The JS engine thread executes the main code block first.

The code executed by each execution stack is a macro task, including the macro task in the task queue (macro task queue). After the macro task in the execution stack is executed, the task in the task queue (macro task queue) will be added to the execution stack, which is also an event cycle mechanism.

If Promise is encountered during macro task execution, a micro task (.then() callback) will be created and added to the end of the micro task queue.

Micro tasks must be created when a macro task is executed. Before the next macro task starts, the browser will re render the page (task > > render > > next task (take one from the task queue)). At the same time, all micro tasks in the current micro task queue will be executed after the execution of the previous macro task and before rendering the page.

That is, after a macro task is executed, all micro tasks generated during its execution will be executed (before rendering) before re rendering and starting the next macro task.

This explains that "promise 1" and "promise 2" are printed before "timer over". "Promise 1" and "promise 2" are added to the micro task queue as micro tasks, and "timer over" is added to the macro task queue as macro tasks. They are waiting to be executed at the same time, but all micro tasks in the micro task queue will be executed before starting the next macro task.

In the node environment, process Nexttick has higher priority than Promise, that is, after the macro task ends, nextTickQueue in the micro task queue will be executed first, and then Promise in the micro task will be executed.

Implementation mechanism:

  1. Execute a macro task (get it from the event queue if it is not in the stack)
  2. If a micro task is encountered during execution, it will be added to the task queue of the micro task
  3. After the macro task is executed, all micro tasks in the current micro task queue will be executed immediately (executed in sequence)
  4. After the execution of the current macro task, start to check the rendering, and then the GUI thread takes over the rendering
  5. After rendering, the JS engine thread continues to start the next macro task (obtained from the macro task queue)

Macro task (task)

An event loop has one or more task queues. Task task sources are very broad. For example, the onload and click events of ajax. Basically, the various events we often bind are task task sources, as well as database operations (IndexedDB). It should be noted that setTimeout, setInterval and setImmediate are also task task sources. In summary, task task source:

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • requestAnimationFrame
  • UI rendering

Micro task (job)

The microtask queue is somewhat similar to the task queue. Both are first in first out queues, and tasks are provided by the specified task source. The difference is that there is only one microtask queue in an event loop. In addition, the timing of microtask execution is different from that of Macrotasks

  • process.nextTick
  • promises
  • Object.observe
  • MutationObserver

Difference between macro task and micro task

  • There can be multiple macro queues and only one micro task queue. Therefore, every time a new settimeout is created, it is a new macro task queue. After a macro task queue is executed, it will go to the checkpoint micro task.
  • After an event cycle, the micro task queue is completed, and then the macro task queue is executed
  • In an event loop, after a macro queue is executed, the micro task queue will be check ed

Macro and micro task examples

1. Add macro task and micro task on the main thread

console.log('-------start--------');

setTimeout(() => {
  console.log('setTimeout');  
}, 0);

new Promise((resolve, reject) => {
  for (let i = 0; i < 5; i++) {
    console.log(i);
  }
  resolve()
}).then(()=>{
  console.log('Promise'); 
})

console.log('-------finish--------');

//-------start--------
//0
//1
//2
//3
//4
//-------finish--------
//Promise
//setTimeout
analysis

First event cycle:

  • The whole script enters the main thread as the first macro task and encounters console Log, output --------- start ---------.
  • When setTimeout is encountered, its callback function is distributed to the macro task Event Queue. Let's record it as setTimeout1 for the time being.
  • When a Promise is encountered, new Promise is executed directly, and the loop outputs 0, 1, 2, 3, 4. Then is distributed to the micro task Event Queue. We call it then1.
  • Continue to the next step. When you encounter clg, you will directly output -------- end --------. This is the end of the first round of event cycle. You will first see whether the current cycle has generated micro tasks, and then execute them in the order of generation.
  • When then1 is found, Promise is output. The current micro task is completed. Here, the first round of event cycle ends.

Find the setTimeout1 macro task and start the second round of event cycle:

  • When a clg is encountered, the setTimeout is output directly. There is no micro task, and the second round of event cycle ends.

All macro tasks have been executed and the whole program has been executed

2. Create a micro task in a micro task

setTimeout(()=> console.log('setTimeout4'))

new Promise(resolve => {
  resolve()
  console.log('Promise1')
}).then(()=> {
  console.log('Promise3')
  Promise.resolve().then(() => {
    console.log('before timeout')
  }).then(() => {
    Promise.resolve().then(() => {
      console.log('also before timeout')
    })
  })
})

console.log('finish')

//Promise1
//finish
//Promise3
//before timeout
//also before timeout
//setTimeout4
analysis

First event cycle:

  • When setTimeout is encountered, its callback function is distributed to the macro task Event Queue. Let's record it as setTimeout1
  • If Promise is encountered, new Promise is executed directly and Promise 1 is output. Then is distributed to the micro task Event Queue. We record it as then1 (be careful not to read the contents of then here).
  • Encountered console Log, end of output
  • Execute micro task then1, encounter clg, output promise 3, encounter promise Resolve() Then, then are distributed to the micro task Event Queue. We call it then2
  • Execute then2 and clg output before timeout to generate the micro task then3
  • Execute then3, and clg outputs also before timeout. So far, the execution of the micro task is completed, and the first round of event cycle ends

Find the setTimeout1 macro task and start the second round of event cycle:

  • Console Log, output setTimeout4, no micro task, the second round of event cycle ends

3. Create macro task in micro task queue

new Promise((resolve) => {
  console.log('new Promise(macro task 1)');
  resolve();
}).then(() => {
  console.log('micro task 1');
  setTimeout(() => {
    console.log('setTimeout1');
  }, 0)
})

setTimeout(() => {
  console.log('setTimeout2');
}, 500)

console.log('========== finish==========');

//new Promise(macro task 1)
//========== finish==========
//micro task 1
//setTimeout1
//setTimeout2
analysis

First event cycle:

  • When a Promise is encountered, new Promise is directly executed and new Promise(macro task 1) is output. Then is distributed to the micro task Event Queue. We call it then1.
  • When setTimeout is encountered, its callback function is distributed to the macro task Event Queue. Record as macro set1
  • clg output =========== end==========
  • Execute micro task then1 and CLG to output micro task 1. When setTimeout is encountered, a macro task is generated and recorded as macro set2. So far, the execution of the micro task is completed and the first round of event cycle is ended

It is found that there are two timer macro tasks. Here, the macro set2 is executed first (the reason why it is executed first is explained in detail below) to start the second round of event cycle:

  • clg output setTimeout1, no micro task, the second round of event cycle ends

Execute macro set1 to start the third event cycle:

  • clg output setTimeout2, no micro task, the third round of event cycle ends

Why does the macro set2 take precedence

Although macro set1 is first processed by the timer triggered thread, the callback of macro set2 will be added to the message queue first.

Above, the delay of macro set2 is 0ms. The HTML5 standard stipulates that the second parameter of setTimeout should not be less than 4 (the minimum value will be different for different browsers). If it is insufficient, it will increase automatically. Therefore, "setTimeout2" will still be after "setTimeout1".

Even if the delay is 0ms, the callback function of macro set2 will be added to the message queue immediately. The callback will be executed when the execution stack is empty (JS engine thread is idle).

In fact, the second parameter of setTimeout does not represent the exact delay event of callback execution. It can only represent the minimum delay time of callback execution, because the callback function needs to wait for the completion of the synchronization task in the execution stack after entering the message queue, and will be executed only when the execution stack is empty.

4. Create a micro task in a macro task

setTimeout(() => {
  console.log('timer_1');
  setTimeout(() => {
    console.log('timer_3')
  }, 0) 
  new Promise(resolve => {
    resolve()
    console.log('new promise')
  }).then(() => {
    console.log('promise then')
  })
}, 0)

setTimeout(() => {
  console.log('timer_2')
}, 0)

console.log('finish')

//finish
// timer_1
//new promise
//promise then
// timer_2
//timer_3
analysis

First event cycle:

  • Create macro set1
  • Create macro set2
  • Console Log output "end"
  • No micro task, current event cycle ends

Execute macro set1 to start the second event cycle:

  • Console Log output "timer_1"
  • Create macro set3
  • new promise, directly output'new promise'to create the micro task then1
  • Execute micro task then1 and output'promise then'
  • No micro task, current event cycle ends

Execute macro set2 to start the third event cycle:

  • clg output'timer_2'
  • No micro task, current event cycle ends

Execute macro set3 to start the fourth event cycle:

  • clg output'timer_3'
  • No micro task, current event cycle ends

Event Bubble + event loop

 <div class="outer">
    <div class="inner"></div>
  </div>

var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

  function onClick() {
    console.log('inner');

    setTimeout(function () {
      console.log('inner-timeout');
    }, 0);

    Promise.resolve().then(function () {
      console.log('inner-promise');
    });

  }
  function onClick2() {
    console.log('outer');

    setTimeout(function () {
      console.log('outer-timeout');
    }, 0);

    Promise.resolve().then(function () {
      console.log('outer-promise');
    });

  }

  inner.addEventListener('click', onClick);
  outer.addEventListener('click', onClick2);

// inner
// inner-promise
// outer
// outer-promise
// inner-timeout
// outer-timeout

 

Event bubbling is triggered from the inside out, so:

(1)click inner,onClick Function into execution stack, print "click". After execution, the execution stack is empty. Because of event bubbling, the event triggering thread will put the task that sends events upward into the macro task queue.
(2)encounter setTimeout,After the minimum delay time, put the callback into the macro task queue. encounter promise,take then Put the task of into the micro task queue
(3)At this point, the execution stack is empty again. Start emptying the micro task and printing "promise"
(4)At this point, the execution stack is empty again. Take a task from the macro task queue for execution, that is, the task of dispatching events mentioned above, that is, bubbling.
(5)Event bubbling to outer,Execute callback and repeat the above steps "click","promise" The printing process of.
(6)Take tasks from the macro task queue for execution. At this time, our macro task queue has accumulated two setTimeout So they will be in two Event Loop It is implemented successively in the cycle.

async+await

    • async returns a promise generator + co
    • Await = > yield if a promise is generated, the promise will be called Then method

 

    async function f1() {
            await f2()
            console.log('f1 finish')
        }
        async function f2() {
            await f3()
            console.log('f2 finish')
        }
        async function f3() {
            console.log('f3 finish')
        }
        f1()
        new Promise(res=>{
            console.log('new Promise')
            res()
        }).then(res=>{
            console.log('promise First then')
        }).then(res=>{
            console.log('promise Second then')
        })

//f3 finish
//new Promise
//f2 finish
//promise First then
//f1 finish
//promise Second then
analysis
    • await in f1 and f2 returns promise, so we can convert it to promise

 

 async function f1() {
            //await f2()
            //console.log('f1 finish')
        new Promise((resolv,reject)=>resolve(f2())).then(()=>{
             console.log('f1 finish')
        })
        }
        async function f2() {
            //await f3()
            //console.log('f2 finish')
             new Promise((resolv,reject)=>resolve(f3())).then(()=>{
             console.log('f2 finish')
        })
        }
        async function f3() {
            console.log('f3 finish')
        }
        f1()
        new Promise(res=>{
            console.log('new Promise')
            res()
        }).then(res=>{
            console.log('promise First then')
        }).then(res=>{
            console.log('promise Second then')
        })
  • When executing f1(), directly execute the f2 method in resolve;
  • Execute f2(), as above, and proceed to f3 for execution;
  • f3 only has the synchronization code clg, and the direct printing of f3 ends;
  • At this point, the resolve() in f2 is completed, and an error is encountered Then generates the first then1 micro task; Execute f1() to stop here and execute next
  • new Promise directly prints new Promise to create a second then2 micro task,
  • After executing the code in the current macro task, clear the micro task queue,
  • Execute then1 and print f2. At this time, f2 is completed, that is, the resolve() of f1 is completed, and the third then3 micro task is created
  • Execute then2, print promise the first then, and create the micro task then4
  • Execute then3, and print f1 ends;
  • Execute then4, print promise the second then,
  • The micro task queue is cleared, and the current event cycle ends

summary

  • From top to bottom, execute directly and synchronously, and distribute macrotasks or microtask s asynchronously
  • Directly execute the MacroTask and put the callback function into the MacroTask execution queue (execute the next event cycle); Execute the microtask directly. Put the callback function into the microtask execution queue (this event is executed circularly)
  • When the synchronization task is completed, execute the micro task. (microtask queue cleared)
  • This leads to the next round of event cycle: execute macro task MacroTask (setTimeout, setInterval, callback)

Finally, let's analyze a piece of complex code to see if you really master the execution mechanism of js:

console.log('1');

setTimeout(function() {
    console.log('2');
    Promise.resolve().then(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
Promise.resolve().then(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
   Promise.resolve().then(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

Answer: 1, 7, 6, 8, 2, 4, 3, 5, 9, 11, 10, 12

 
Original text from Zhihu: https://zhuanlan.zhihu.com/p/139967525

 

Posted by booga1138 on Tue, 31 May 2022 13:38:38 +0530