Yet Another Intro to Event Loop

As we tell, there're tons of posts talking about event loop, the basic of the basic knowledge of JavaScript running mechanism.

Long long ago, event loop was even simple, pure and naive. As time goes by, it has became more fancy as we see for now, and more powerful and performant at the same time.

It's not an easy job to cover all details of event loop, so this post would focus on the content as listed:

  • What does event loop look like?
  • What are multiple macro queues with prioritization for?
  • Micro queue? I have heard that, but what is it for?

What does event loop look like?

Event loop is used to coordinate events, user interaction, scripting, network tasks, etc.
It's said to be constantly running process that keeps a tab on the execution stack, when the execution task turns out to be available (that's there is empty), event loop will proceed to execute all callback functions inside the queues. And roughly there're two categories of queues with different priorities and behaviors, namely macro queue and micro queue.

What are multiple macro queues with prioritization for?

Maybe you have heard of task queue or event queue before. Yuh, it's the same thing with macro queue, which is used to diff from micro queue for the modern browsers.

The task inside macro queue represents a discrete and independent task which should be executed asynchronously. On initialization, the JavaScript engine first pulls off the first task from macro queue and perform the corresponding callback function, if there's a call to native API module with asynchronous callback function (AJAX, fetch, setTimeout, document.addEventListener, etc.), the module will push them to macro queue at the right time. It's worth mentioning that once inside the macro queue, each task is required to wait to the next round of event loop at least.

Please note that, there is only one macro task can be executed per round of event loop. And this is the most significant difference to micro queue when you make a comparison between them, I guess.

And as the title said, there is not only one macro queue per browsing context. One for mouse and keyboard events that are triggered by users and assigned with third-quarters of priority on the premise of keeping the tasks order, so as to ensure that users' interaction gets the highest priority response, while other queues are prepared for other tasks. Although the other queues just get first-quarter priority, but spec has specified that the browsers should optimize the algorithm to keep the interface responsive while avoiding starving them to death.

Here's an extreme example, show how callback of setTimeout gets stuck by endless scrolling.

let start = null
document.addEventListener('scroll', evt => {
    if (!start) {
        start = (+new Date())
        setTimeout(() => console.log((+new Date()) - start), 1000)
    }
})

Since the scroll event handler has kind of privilege comparing with setTimeout, while we do keep scrolling more than a second, the elapse of time might be far from the configured 1000 milliseconds.

And now we have learn a lot of macro queue, but how to access macro queues? As mention before, it includes setTimeout/setInterval, setImmediate, requestAnimationFrame, events, network tasks ans so forth. Except native Promise.then and MutationObserver, almost all calling to API modules with asynchronous callback function could push macro task to its corresponding queues.

How About Micro Queue?

Micro queue is often scheduled for things that are required to be completed immediately after the execution of current script. On completion of one macro task, event loop will move on to the micro queue and wouldn't move to the next task outside micro queue until all tasks of micro queue are completed. Because event loop will keep calling on micro queue until there are no more tasks left, even if new tasks keep getting added. It's said that micro tasks enqueued in current round of event loop would be processed in one fell swoop. For example,

p = new Promise(resolve => {
    resolve()
})
p.then(() => {
    console.log(1)
    p.then(() => {
        console.log(2)
    })
})

console.log(1) and console.(2) are executed in the same round of event loop one by one. And once all tasks inside micro queue are finished, then event loop will shift back to macro queue.

The important reason for using micro queue is to ensure consistent order of tasks, avoid logic sprinkling on many small tasks under mid-execution, simultaneously reducing the risk of delay cause by users.

So if you want to execute something after current script at once, micro task will be perfect for you.

Recap

Now, we have learn event loop, macro queue and micro queue in detail. Event loop is just like a babysitter, who will keep its eyes on the execution stack, once it's empty, event loop will have a phone call to micro queue "Hi, there! It's your show time 😃". When micro queue are tired and there are no any more tasks left, it's macro queues turn, while the user interaction related macro tasks are privileged.

If you like this article, chances you'd like what I tweet as well. If you are curious, have a look at my weibo profile.

posted @ 2021-10-26 19:02  ^_^肥仔John  阅读(17)  评论(0编辑  收藏  举报