preface

Being familiar with event loops and how browsers work will help us understand JavaScript execution and troubleshoot runtime problems. Here are some principles and examples of browser event loops summarized below.

Browser JS asynchronous execution principle

JS is single-threaded, meaning only one thing can be done at a time, so why can browsers perform asynchronous tasks at the same time?

Because browsers are multithreaded, when the JS needs to perform an asynchronous task, the browser will start another thread to perform the task.

In other words, JS is single-threaded means that there is only one thread executing JS code, which is the main thread of the JS engine provided by the browser. Browsers also have timer threads, HTTP request threads, etc. These threads are not primarily designed to run JS code.

For example, if the main thread needs to make an AJAX request, another browser thread (the HTTP request thread) will actually send the request, and when the request comes back, the JS callback will be executed by the JS engine thread.

The browser is actually responsible for sending the request, and JS is only responsible for the final callback processing. So the asynchrony here is not the IMPLEMENTATION of JS itself, in fact, is the ability provided by the browser.

Event loops in the browser

Execution stack and task queue

When parsing a piece of code, JS puts synchronized code somewhere in order, that is, the execution stack, and then executes the functions inside it in turn.

When an asynchronous task is encountered, it is handed over to other threads to handle it. After all the synchronous code in the current execution stack is executed, the callback of the completed asynchronous task will be removed from a queue and added to the execution stack to continue execution.

When an asynchronous task is encountered, it is handed over to another thread,….. And so on.

When other asynchronous tasks complete, callbacks are placed on the task queue to be executed stack.

Macro and micro tasks

According to different types of tasks, it can be divided into micro task queue and macro task queue.

During the event cycle, the execution stack first checks whether there is any task to be executed in the microtask queue after the synchronous code execution is completed. If not, it checks whether there is any task to be executed in the macro task queue again and again.

Microtasks are usually executed first in the current loop, while macro tasks wait until the next loop. Therefore, microtasks are usually executed before macro tasks, and there is only one microtask queue, while macro task queues may be multiple. Other common events such as clicks and keyboards are also macro tasks.

Common macro tasks:

  • setTimeout()
  • setInterval()
  • UI interaction events
  • postMessage
  • setImmediate() — nodeJs

Common microtasks:

  • Promise. Then (), promise. The catch ()
  • new MutaionObserver()
  • process.nextTick() — nodeJs

The following is an example:

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

new Promise((resolve) = > {
  console.log('Sync Code 2')
  resolve()
}).then(() = > {
    console.log('promise.then')})console.log('Sync code 3');

// Final output "sync code 1"," sync code 2", "sync code 3", "promise.then", "setTimeout"
Copy the code

Specific analysis is as follows:

  1. Both setTimeout callbacks and promise.then are executed asynchronously and will be executed after all synchronized code;

  2. While promise.then follows, the order of execution takes precedence over setTimeout because it is a microtask;

  3. The new Promise is executed synchronously, while the promise.then callback is asynchronous.

Note: when setTimeout is set to 0 in the browser, it defaults to 4ms and NodeJS to 1ms.

Essential differences between microtasks and macro tasks:

  • Macro task characteristics: There are explicit asynchronous tasks to execute and call back; Additional asynchronous thread support is required.

  • Microtask characteristics: There are no explicit asynchronous tasks to execute, only callbacks; No additional asynchronous thread support is required.

Run order of Async/await

The characteristics of

  1. The async declared function simply wraps the return of the function so that a promise object will be returned anyway (non-promises will be converted to a Promise {resolve}).

  2. The await declaration can only be used in async functions.

    • When an async function is executed and an await declaration is encountered, the ‘normal execution rules’ shall be followed by an await declaration.

    • Immediately exit async and execute the rest of the main thread. Wait until the main thread is finished and then go back to the await location to continue.

The sample

const a = async () => {
  console.log("a");
  await b();
  await e();
};

const b = async () => {
  console.log("b start");
  await c();
  console.log("b end");
};

const c = async () => {
  console.log("c start");
  await d();
  console.log("c end");
};

const d = async () => {
  console.log("d");
};

const e = async () => {
  console.log("e");
};

console.log('start');
a();
console.log('end');
Copy the code

The results

Individual analysis

  1. In the current synchronization environment, console.log(‘start’) is executed; Output ‘start’.

  2. When the synchronization function a() is encountered, execute a().

  3. A () is a sync/await construct, console.log(“a”) outputs ‘a’, declares’ await b() ‘, is an asynchronous function, executes inside function b(). (similar operation, I thought myself) and push the contents after await b() to the microtask queue. We can say [await b()].

  4. B () is a sync/await construct, executes sequence. console.log(“c start”) outputs ‘c start’, declares await c() when await, is an asynchronous function, executes inside function C (). And push the contents after await c() to the microtask queue. We can say [await c(), await b()].

  5. C () is sync/await construction, sequential execution encounters console.log(“b start”) outputs ‘b start’, encounters await declaration await d(), is an asynchronous function, executes inside function D (). And push the contents after await d() to the microtask queue. We can say [await d(), await c(), await b()].

  6. In d(), the sequential execution encounters console.log(“”) and outputs ‘d’, and the d() function finishes running.

  7. After executing d(), there is no asynchronous function to execute, so it enters the synchronous environment and executes a().

  8. If console.log(“end”) is encountered, output ‘end’. At this time, the main thread in the synchronization environment is finished, and check whether there are microtasks in the microtask queue.

  9. There is a microtask in the queue [await d(), await c(), await b()], which executes the content after await d().

  10. The content after await d() is console.log(“c end”), output ‘c end’. At this point the content is finished, and then check from the microtask queue [await c(), await b()], and execute the content after await c().

  11. After await c(), console.log(“b end”); , output ‘b end’. At this point the content is finished, and then check from the microtask queue after [await b()], and execute the content after await b().

  12. After await d(), await e() is encountered with await declaration, execute e(). And judge and await e() after no running code, need not enter micro task queue.

  13. Execute e(), console.log(“e”) sequentially; , output ‘e’. That’s when the function ends.

  14. If [] does not contain any microtask in the microtask queue, the execution is complete. The synchronization environment is displayed.