JavaScript event loop

I. Event cycle

An event loop contains not only event queues, but at least two queues that hold other operations performed by the browser in addition to events. These operations are called tasks and fall into two categories: macro tasks (or tasks as they are commonly called) and microtasks. Let’s first look at what macro and micro tasks are.

  1. Macro tasks: Include creating the main document object, parsing HTML, executing the main (or global) JavaScript code, changing the current URL, and various events such as page loading, input, network events, and timer events.
  2. Microtasks: Microtasks are smaller tasks. Microtasks update the state of the application, but must be performed before the browser tasks continue to perform other tasks, including rerendering the UI of the page. Examples of microtasks include promise callbacks, DOM changes, and so on. Microtasks need to be executed as quickly as possible, asynchronously, and without creating entirely new microtasks.

The implementation of the event loop should have at least one queue for macro tasks and at least one queue for micro tasks.

Event loops are based on two basic principles:

  • Tackle one task at a time
  • Once a task is started, it will not be interrupted by other tasks until it is finished

The above two principles can be easily understood by looking at the graph below:

The event loop first checks the macro task queue, and if the macro task is waiting, execution of the macro task begins immediately. Until the task finishes running (or the queue is empty), the event loop moves to process the microtask queue. If there are tasks waiting in the queue, the event loop will start executing one by one, and then execute the remaining microtasks until all the microtasks in the queue are completed. Note the difference between processing macro tasks and microtask queues: in a single iteration of the loop, at most one macro task is processed (the rest wait in the queue), while all microtasks in the queue are processed.

The above introduction is quite abstract, you may not know what it is, and how to execute between them, so, let’s take a look at the examples.

<button id="firstButton"></button>
<button id="secondButton"></button>
<script>
 const firstButton = document.getElementById("firstButton");const secondButton = document.getElementById("secondButton"); firstButton.addEventListener("click".function firstHandler(){Promise.resolve().then(() = >{/*Some promise handling code that runs for 4 ms*/}); ⇽-- Immediately object promise and execute the callback function in the then method/*Some click handle code that runs for 8 ms*/}); secondButton.addEventListener("click".function secondHandler(){/*Click handle code that runs for 5ms*/});/*Code that runs for 15ms*/
</script>
Copy the code

We assume that the following behavior occurs:

  • Ms 5 clickfirstButton.
  • 12 ms clicksecondButton.
  • firstButtonClick event handler offirstHandlerYou need to perform 8ms.
  • secondButtonClick event handler ofsecondHandlerYou need to perform 5ms operations.

When running 15ms, the macro task queue with firstHandler and secondHandler clicked the button at 5ms and 12ms respectively, because the micro task queue at this time is empty, so the event loop executes the next queue of the macro task queue. That is, firstHandler, creates and immediately fulfills the promise with its then callback into the microtask queue. As soon as the first click event completes, the promise object’s success callback is executed, while the second click event, secondHandler, continues to wait in the macro task queue. When the task in the microtask queue is empty, the event loop starts to re-render the page and continue with the second button click task.

Two, the use of timer

Browsers provide two methods for creating timers: setTimeout and setInterval. The browser also provides two corresponding clearing timer methods: clearTimeout and clearInterval. These methods are all methods mounted on the Window object (global context).

  • setTimeoutStarts a timer and executes a callback function at the end of the specified delay time that returns a unique value identifying the timer
  • setIntervalStarts a timer and executes the callback function at the specified delay interval until it is cancelled. Returns a unique value that identifies the timer

To execute a timer in an event loop, look at the following example:

<button id="myButton"></button>
<script>
 setTimeout(function timeoutHandler(){/*Some timeout handle code that runs for 6ms*/},10); ⇽-- Delayed execution of functions after 10ms registrationsetInterval(function intervalHandler(){/*Some interval handle code that runs for 8ms*/},10); ⇽-- Register periodic functions executed every 10msconst myButton = document.getElementById("myButton"); myButton.addEventListener("click".function clickHandler(){/*Some click handle code that runs for 10ms*/}); ⇽-- Registers event handlers for button click events/*Code that runs for 18ms*/
</script>
Copy the code

Assumptions, users click on the button, when 6 ms of the macro task queue at this time, there are two events, one is to perform JS main thread code, the other is a click event, when the 10 ms, delay timer expires, the first time interval of interval timer trigger, at that time, macro task queue have four tasks, respectively is: Execute JS main thread code, stand-alone event, delay timer expiration event, interval timer trigger event, because the main thread code has not finished executing; At 18ms, the main thread task ends. Since there are no microtasks, the next macro task, the button click event, is executed. At 20ms, the interval timer fires once, but the instance of the interval timer is already in the queue, so the trigger stops and the browser does not create two identical interval timers. The click event ends at 28ms, when it is the turn of the delay timer to execute. As you can see from this point, we can only control when the timer is enqueued, not when it is executed, because the delay timer is executed after 10ms and now at 28ms. The delay timing processor takes 6ms and will end execution at 34ms. During this time, another interval timer expires at 30ms. Again, no new interval timer will be added to the queue because there is already a matching interval timer in the queue.

The interval timing processor starts execution at 34ms, 24ms away from adding to the queue. Again, the setTimeout(fn, delay) and setInterval(fn, delay) parameters are passed to specify only when the timer is added to the queue, not the exact execution time.

The interval timer processor needs to execute 8ms, and when it executes, another interval timer expires at 40ms. At this point, because the interval processor is executing (not waiting in a queue), a new interval timing task is added to the task queue and the application continues execution

The important concept to remember is that event loops can only handle one task at a time, and we can never be sure that the timer handler will execute for the exact time we expect. This is especially true of interval handlers. In this example we see that although we scheduled the interval to fire at 10, 20, 30, 40, 50, 60, and 70ms, the callback is executed at 34, 42, 50, 60, and 70ms. In this case, the callback was executed twice less, and several times the callback did not execute at the expected point in time.

Here’s an example to practice:

Run the following code, what is output after 2s?setTimeout(function () {
  console.log('Timeout ')},1000)
setInterval(function () {
  console.log('Interval ')},500)
// Interval Timeout Interval Interval Interval
Copy the code

The setInterval method calls the processor at at least a fixed interval until it explicitly clears the interval timer. The setTimeout method, on the other hand, calls the callback only once after the specified timeout expires. In this case, the first setInterval callback is called once at 500ms. The setTimeout function is then called at 1000ms, and another setInterval is immediately executed. The other two setIntervals were executed at 1500ms and 2000ms, respectively.

Let’s look at another example:

Run the following code, what is output after 2s?const timeoutId = setTimeout(function () {
  console.log('Timeout ')},1000)
setInterval(function () {
  console.log('Interval ')},500)
clearTimeout(timeoutId)
// Interval Interval Interval Interval
Copy the code

Explanation: The setTimeout callback is cleared before it has a chance to execute, so only four setInterval callbacks are executed in this example.

The difference between executing setTimeout late and executing setInterval at intervals

Interval execution looks like a periodic repetition of delayed execution, but the difference is not that:

setTimeout(function repeatMe(){/* Some long block of code... * /
 setTimeout(repeatMe, 10);
}, 10); ⇽-- register the delayed task and re-execute itself every 10mssetInterval(() = >{/* Some long block of code... * /
}, 10); ⇽-- registers periodic tasks. The tasks are executed once every 10msCopy the code

Two pieces of code appear to be functionally equivalent, but they are not. Obviously, the code in setTimeout will be delayed at least 10ms after the previous callback completes (depending on the state of the event queue, the wait time will only be greater than 10ms); SetInterval tries to execute the callback every 10ms, regardless of whether the previous callback was executed.

Third, summary

To close the event loop with a question, run the following code and what will be printed?

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')}async function async2() {
  console.log('async2')}console.log('script start')

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

async1()

new Promise((resolve) = > {
  console.log('promise1')
  resolve()
}).then(() = > {
  console.log('promise2')})console.log('script end')
Copy the code

Correct answer:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
Copy the code

Here’s a quick explanation: SetTimeout will be put into the macro task queue and wait for the next event loop to execute. At this time, it will execute to await async2(). It prints async2, but await returns a Promise, Promise. Then, so it puts it in the microtask queue, so it jumps out of async1 and executes to new Promise and prints promise1, And immediately, but the callback function in its then is put into the microtask queue, and then executed to the end, printing script end.

At this time, the JS main thread code task is finished, and the task in the microtask queue will be executed. As can be seen from the above, there are two microtasks at this time. When the first one is executed, it will return to async1 and then execute, and output async1 end. The new Promise callback outputs promise2, and then the microtask queue completes, and the event loop starts executing the next macro task queue, which is the setTimeout callback, and then outputs setTimeout.

And by the way, they use 0 as the timeout. If you look at how the event loop works, you know that this does not mean that the callback will be executed at 0ms. Using 0 means that the browser is notified to perform the callback as soon as possible, but unlike other microtasks, the page rendering can be performed before the callback.