First of all, JavaScript is a single-threaded scripting language.


So that is to say in the process of one line of code execution, there must not exist at the same time the execution of another line of code, like the use of alert() after the madness of console.log , if you do not close the pop-up box, the console will not show a log message.


Or maybe some code performs a lot of calculations, let’s say brute force password cracking on the front end or some other ghost operation, which causes the subsequent code to keep waiting and the page to be in a fake dead state because the previous code didn’t finish executing.


So if all the code is executed synchronously, this can cause serious problems, let’s say we want to get some data from the remote end, do we have to keep looping the code to determine whether we got the return result? Just like going to a restaurant to order food, surely can not say that after ordering to go to the kitchen to urge people to fry the food, will be punched.


So there’s the concept of asynchronous events, registering a callback function to, say, send a network request, and we tell the main program to wait until it receives the data and notify me, and then we can go do something else.


Then we are notified when the asynchrony is complete, but the program may be doing something else at that point, so even if the asynchrony is complete you need to wait around until the program is free to see which asynchronies have completed and can be executed.


For example, if you take a taxi, if the driver arrives first, but you still have a little something to deal with at hand, then the driver is unlikely to drive away first by himself, and must wait until you have finished dealing with the matter and get into the car before he can go.

 Differences between microtasks and macrotasks

 This is just like going to the bank to do business, you first have to take a number for the queue.

It’s usually printed on the top with something like, “Your number is XX and there are XX other people in front of you.” Something like that.


Because the teller at the same time functions to deal with a customer who comes to do business, this time each person who comes to do business can be considered to be a bank teller’s a macro task to exist, when the teller to deal with the current customer’s problem, choose to receive the next one, broadcasting the number of the report, that is, the beginning of the next macro task.


So multiple macro tasks together can be thought of as saying there’s a task queue here with all the currently scheduled customers in the bank.


Task queue in the asynchronous operations have been completed, rather than registering an asynchronous task will be placed in this task queue, as in the bank in the queue, if you are called when you are not there, then your current number plate will be invalidated, the teller will choose to skip the next customer’s business processing, and so you need to come back to re-take the number!


And a macro task in the process of execution, it is possible to add some micro-tasks, like at the counter for business, you front of an old man may be in the deposit, in the deposit of this business after the transaction, the teller will ask the old man there is no other need to handle the business, this time the old man thought for a moment: “Recently, the P2P explosion a little bit more, is it necessary to choose some stable The first thing you need to do is to get the money from the bank, and then tell the teller that you need to do some financial business, and then the teller will not be able to tell you that you need to get a number and queue up again.


So if it’s your turn to come in soon, you’ll be pushed back because of the “financial services” that the boss added at the last minute.


Maybe the old man would like to get a credit card after he’s done with his finances? Or buy some more coins?


Whatever needs the teller can help her with, she will come in and do those things before she handles your business, and these can be considered microtasks.

 It just goes to show you: your master will always be your master.

The next macro task will not be executed until the current microtask has been executed.


So there’s that code snippet that’s often in interview questions and various blogs:

setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
})

console.log(2)


setTimeout is to exist as a macro task, and Promise.then is a representative micro task, the execution order of the above code is output according to the serial number.

 All the asynchrony that will go into it refers to that part of the code in the event callbacks

That is, the code executed during the instantiation of new Promise is done synchronously, while the callbacks registered in then are executed asynchronously.


It is only after the synchronous code has finished executing that it goes back and checks to see if any asynchronous tasks have completed and executes the corresponding callbacks, which in turn are executed by the microtasks before the macrotasks.


So it gives the above output conclusion 1、2、3、4 .

 The + section indicates the code for synchronized execution

+setTimeout(_ => {
-  console.log(4)
+})

+new Promise(resolve => {
+  resolve()
+  console.log(1)
+}).then(_ => {
-  console.log(3)
+})

+console.log(2)


Originally setTimeout had already set the timer first (equivalent to fetch), and then some Promise processing was added to the current process (temporary addition of business).


So advanced, even if we continue to instantiate Promise in Promise , its output will still predate the macro task at setTimeout :

setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
  Promise.resolve().then(_ => {
    console.log('before timeout')
  }).then(_ => {
    Promise.resolve().then(_ => {
      console.log('also before timeout')
    })
  })
})

console.log(2)


Of course, in practice, there will rarely be a simple call to Promise , usually there will be other asynchronous operations inside, such as fetch , fs.readFile and other operations.


And these are really the equivalent of signing up for a macro task, not a micro task.


P.S. In the Promise/A+ specification, implementations of Promise can be either microtasks or macrotasks, but the general consensus says (at least Chrome does) that Promise should be in the microtask camp


So it becomes critical to understand which operations are macro tasks and which are micro tasks, which is the more popular term in the industry today:

#browserNode
I/O
setTimeout
setInterval
setImmediate
requestAnimationFrame


Some places list UI Rendering as a macro task, but after reading the HTML specification document, it’s clear that this is a parallel step to microtasking


requestAnimationFrame It is also considered a macro task, requestAnimationFrame is defined in MDN as the action to be performed before the next page redraw, and the redraw exists as a step of the macro task, and this step is later than the execution of the micro task.

#browserNode
process.nextTick
MutationObserver
Promise.then catch finally

 What is Event-Loop?

 The OP has been discussing macro-tasks, micro-tasks, the execution of various tasks.

But back to reality, JavaScript is a single-process language and can’t handle multiple tasks at the same time, so when to execute a macro task and when to execute a micro task? We need to have such a judgment logic present.


After each transaction, the teller will ask the current customer if there are any other transactions that need to be done. (Checking that there are no more microtasks to be processed)


And after the customer clearly informs that there is nothing to do, the teller goes to check if there is anyone else waiting to do business in the back. (Ending this macro task, checking to see if there are any more macros to be processed)


This process of checking is ongoing, once for each completed task, and such an operation is called Event Loop . (This is a very simple description, in reality it’s much more complicated)


And as stated above, a teller can only handle one thing at a time, even if those things are requested by a single customer, so it can be assumed that a queue exists for microtasks as well, roughly along these lines:

const macroTaskList = [
  ['task1'],
  ['task2', 'task3'],
  ['task4'],
]

for (let macroIndex = 0; macroIndex < macroTaskList.length; macroIndex++) {
  const microTaskList = macroTaskList[macroIndex]
  
  for (let microIndex = 0; microIndex < microTaskList.length; microIndex++) {
    const microTask = microTaskList[microIndex]

 
    if (microIndex === 1) microTaskList.push('special micro task')
    
 
    console.log(microTask)
  }
 
  if (macroIndex === 2) macroTaskList.push(['special macro task'])
}

// > task1
// > task2
// > task3
// > special micro task
// > task4
// > special macro task


The reason why two for loops are used to represent this is because it is easy to do something like push inside the loop (add some tasks) so that the number of iterations can be dynamically increased.


As well as to be clear, Event Loop is just responsible for telling you which tasks to perform, or which callbacks are triggered, the real logic is still performed in the process.

 Performance in Browser


Having briefly explained the difference between the two tasks above, and what Event Loop does, what is the behavior in a real browser?


The first thing to make clear is that the macro task is necessarily executed after the microtask (since the microtask is actually one of the steps in the macro task)


I/O This one feels a bit general, there are too many things that could be called I/O , clicking once button , uploading a file, interacting with a program all of which could be called I/O .

 Suppose there are some of these DOM structures:

<style>
  #outer {
    padding: 20px;
    background: #616161;
  }

  #inner {
    width: 100px;
    height: 100px;
    background: #757575;
  }
</style>
<div id="outer">
  <div id="inner"></div>
</div>
const $inner = document.querySelector('#inner')
const $outer = document.querySelector('#outer')

function handler () {
  console.log('click') 

  Promise.resolve().then(_ => console.log('promise')) 

  setTimeout(_ => console.log('timeout'))  

  requestAnimationFrame(_ => console.log('animationFrame'))  

  $outer.setAttribute('data-random', Math.random())  
}

new MutationObserver(_ => {
  console.log('observer')
}).observe($outer, {
  attributes: true
})

$inner.addEventListener('click', handler)
$outer.addEventListener('click', handler)


If you click on #inner , the order of execution must be: click -> promise -> observer -> click -> promise -> observer -> animationFrame -> animationFrame -> timeout -> timeout .


Because one time I/O creates a macro task, that means it will go to trigger handler in this task.


According to the comments in the code, after the synchronized code has been executed, this time it will check if there are microtasks that can be executed, and then it found two microtasks, Promise and MutationObserver , and then executed them.


Since the click event bubbles, the corresponding I/O will trigger the handler function twice (once at inner and once at outer ), so it will prioritize the execution of the bubbling event (before the other macro tasks), i.e. it will repeat the above logic.


After executing the synchronization code with the microtasks, at this point continue to look backwards to see if there are any macro tasks.


One thing to note is that because we triggered setAttribute , we actually modified the properties of DOM , which causes the page to be redrawn, and this set action is executed synchronously, meaning that the callback for requestAnimationFrame is executed before setTimeout .

 A few little surprises.


Using the above sample code, if the manual click on the DOM element of the trigger to $inner.click() , then you will get different results.


The order of output under Chrome is roughly like this:


click -> click -> promise -> observer -> promise -> animationFrame -> animationFrame -> timeout -> timeout .


The reason why the order of execution of click is different from that of our manual triggering is this, because it is not a triggering event realized by the user by clicking on the element, but something like dispatchEvent , which I personally don’t think can be regarded as a valid I/O , and after executing the handler callback once to register the microtasks, and registering the macrotasks, the outer $inner.click() is actually not finished executing.


So before the microtask is executed, it has to continue to bubble up to execute the next event, i.e., triggering handler a second time.


So it outputs a second click and waits until both handler are executed before checking for microtasks and macrotasks.

 Two things to keep in mind:


  1. .click() This way of triggering the event is similar to dispatchEvent , which can be interpreted as synchronized code execution.
document.body.addEventListener('click', _ => console.log('click'))

document.body.click()
document.body.dispatchEvent(new Event('click'))
console.log('done')

// > click
// > click
// > done

  1. MutationObserver listener will not say that it will be triggered multiple times at the same time, and only one callback will be triggered for multiple modifications.
new MutationObserver(_ => {
  console.log('observer')
 
}).observe(document.body, {
  attributes: true
})

document.body.setAttribute('data-random', Math.random())
document.body.setAttribute('data-random', Math.random())
document.body.setAttribute('data-random', Math.random())

// 只会输出一次 ovserver


It’s like going to a restaurant and ordering and the waitress yells three times, XX number of beef noodles, doesn’t mean she’s going to give you three bowls of beef noodles.


The ideas above are taken from Tasks, microtasks, queues and schedules, which has an animated version of the explanation in the text.

 Representation in Node


Node is also single-threaded, but handles Event Loop slightly differently than browsers; here’s the address of the official Node documentation.


In terms of understanding the API alone, Node has added two new methods that can be used: process.nextTick for microtasks and setImmediate for macrotasks.


Difference between setImmediate and setTimeout


As defined in the official documentation, setImmediate is called once after Event Loop has finished executing.


setTimeout Instead, it is executed by calculating a delay time after which it is executed.


However, it was also mentioned that if both operations are executed directly in the main process, it is difficult to guarantee which one will trigger first.


This is because if two tasks are first registered in the main process, and then the executed code takes longer than XXs , and the timer is already in the state of executable callbacks.


So the timer will be executed first, and only after the timer has been executed Event Loop will setImmediate be executed.

setTimeout(_ => console.log('setTimeout'))
setImmediate(_ => console.log('setImmediate'))


If you are interested, you can test it yourself, and you will really get different results if you execute it many times.


But if you add some code later, you can make sure that setTimeout will be triggered before setImmediate :

setTimeout(_ => console.log('setTimeout'))
setImmediate(_ => console.log('setImmediate'))

let countdown = 1e9

while(countdown--) { }  


If in another macro task, necessarily setImmediate is executed first:

require('fs').readFile(__dirname, _ => {
  setTimeout(_ => console.log('timeout'))
  setImmediate(_ => console.log('immediate'))
})
 

process.nextTick


As said above, this can be thought of as a microtask implementation similar to Promise and MutationObserver , where nextTick can be inserted at any time during code execution and will be guaranteed to be executed before the next macro task starts.


One of the most common examples of usage is the operation of some event binding classes:

class Lib extends require('events').EventEmitter {
  constructor () {
    super()

    this.emit('init')
  }
}

const lib = new Lib()

lib.on('init', _ => {
 
  console.log('init!')
})


Because the above code is executed synchronously when instantiating the Lib object, the init event is sent immediately after the instantiation is complete.


At this point in time, the main program in the outer layer has not yet begun to execute to the point where lib.on('init') is listening for events.


So it will result in sending the event without a callback, and the event will not be sent again after the callback is registered.


We can easily use process.nextTick to solve this problem:

class Lib extends require('events').EventEmitter {
  constructor () {
    super()

    process.nextTick(_ => {
      this.emit('init')
    })
 
  }
}


This will trigger the Event Loop process to find out if there are any microtasks when the program is idle after the code of the main process has finished executing, and then send the init event.


Regarding the fact that some articles mention that calling process.nextTick in a loop will cause an alarm and the subsequent code will never be executed, this is correct, see the double-loop implementation of loop above, which is equivalent to doing push on the array every time the for loop is executed so that the loop will never end.

 A word about the async/await functions.


The reason is that async/await is essentially a wrapper around Promise , while Promise is a type of microtask. So using the await keyword has the same effect as Promise.then :

setTimeout(_ => console.log(4))

async function main() {
  console.log(1)
  await Promise.resolve()
  console.log(3)
}

main()

console.log(2)


The code before the async function’s wait is executed synchronously, which can be interpreted as the code before the wait belongs to the code passed in at new Promise , and all the code after the wait is the callback in Promise.then .

By lzz

Leave a Reply

Your email address will not be published. Required fields are marked *