The premise

As front-end programmers, we’ve certainly heard the following description of the javascript language:


J a v a S c r i p t A single-threaded, asynchronous, non-blocking, interpreted scripting language.” “JavaScript is a single-threaded, asynchronous, non-blocking, interpreted scripting language.”

Now let’s explain what that means.

Single thread

Single thread: Only one thread is responsible for executing code in the JS execution environment. This thread can be called the main thread.

The reason for single-threaded browsers: The early javascript language was a footstep language that ran on the browser. His purpose is the dynamic interaction of the page, the core of page interaction is dom operation. This dictates that it must operate with a single thread, or complex multithreading problems can occur. Imagine if JS is multi-threaded and one thread modifies one OF the DOM elements and another thread removes it, the browser doesn’t know which one is the best.

Disadvantages: You encounter a time-consuming task and must wait for the time-consuming operation to complete. Results in the execution of the entire program.

console.time('timing')
console.log('start.... ')
for(let i = 0; i < 2000000000; i++) {
}
console.log('end.... ')
console.timeEnd('timing')
Copy the code

We run the above code and find:

  1. The printing of end must wait for the loop to complete
  2. The browser cannot do anything during the loop

At the same time, HTML5 proposes Web Worker, which allows javascript to create multiple sub-threads, but the sub-threads are completely controlled by the main thread and cannot operate dom. So child threads can do some time-consuming calculations, as shown in 🌰 :

				// The page freezes
        console.log('script end')
        let count = 0
        setTimeout(() = > {
            for(let i = 0; i < 2000000000; i++) {
                count += 1
            }
            console.log(count)
        }, 1000)
        console.log(count)

        // Do not freeze the page
        function getdata() {
            console.log('this'.this)
            let count = 0
            for(let i = 0; i < 2000000000; i++) {
                count += 1
            }
            this.postMessage(count)
        }
        function createWorker(f) {
            var blob = new Blob(['(' + f.toString() + ') () '])
            var url = window.URL.createObjectURL(blob)
            var worker = new Worker(url)
            return worker
        }

        const pollingWorker = createWorker(getdata)

        pollingWorker.onmessage = function(e) {
            console.log('e', e)
        }
Copy the code

When we write code that has that kind of time consuming operation we can use child threads to calculate it without causing the page to stall.

Web Worker reference this article:

Browser multithreading

Here’s a misconception I’ve had for three quarters of my career: that browsers run as a single thread. That’s not right.

When we say JS is a single thread, it’s just one thread in the browser – the JS engine thread. In fact, the current browser is mostly multithreaded, even if the single thread is estimated to have eliminated no one to use it.

A browser usually has the following resident threads:

  • Rendering engine: responsible for rendering pages
  • JS engine thread: responsible for PARSING and executing JS
  • Timer trigger thread: Handles timer events, such as setTimeout setInterval
  • Event-triggered thread: Handles DOM events
  • Asynchronous HTTP threads: Handle HTTP requests

In the single thread test above, the reason js is looping a lot of code and the browser can’t do anything is because the render thread and the JS engine thread are mutually exclusive.

So, although there is only one thread running JS code, browsers provide other threads. Some IO operations, timer tasks, click events, and screen rendering are all done by threads provided by the browser.

It is these threads, in conjunction with our event queues, that make javascript asynchronous. I’ll leave the details of how this works to the event queue.

Synchronous/asynchronous

Synchronization: Synchronization does not mean that only the code runs at the same time, but that the code must run line after line. The next line is not run until the previous line has the desired result.

debugger
console.log('script start')
function bar () {
  debugger
  console.log('bar task')}function foo() {
  debugger
  console.log('foo task')
  bar()
}
foo()
console.log('script end')
Copy the code

You can look at the call Stack in the browser and see that the call stack is loaded in order of execution, and then unloaded.

Asynchrony: Asynchrony means that a program invocation does not immediately yield results and does not block subsequent code execution. The caller does not have to actively query the result. When the asynchronous task 🈶️ receives the result, it actively notifies the caller.

Asynchrony is more about cooperating with other threads and event queues. Let’s get into the loop of events.

The event loop even-loop/ message queue

Most people may have a misconception: with asynchrony, the page will not be stuck, as long as we put the task to the asynchrony will not be stuck. Is it really so?

The answer is no, why do we introduce child threads? Because asynchronous tasks will eventually be called by the JS engine. Time-consuming operations that are asynchronous can also stall the page.

console.time('count')
let count = 0
function test() {
	// debugger
  for(let i = 0; i < 2000000000; i++) {
    count += 1
  }
  console.endTime('count')}setTimeout(() = > {
	test()
}, 1000)
Copy the code

When I run this code they find that the page is stuck and the screen is blank. Come and print count after 1000ms. If we add the debugger to Test, the test function will appear in the browser’s Call stack. This is proof enough that asynchronous tasks will eventually run into the main thread.

Why is that? Take a look at the diagram below to illustrate the event cycle.

The main thread is the code that our JS engine performs synchronously, including variable/function declarations/assignments, etc.

Asynchronous tasks are initiated when our main thread code triggers another thread’s task, such as when we initiate an AJAX thread, or trigger a timer task, or change the page layout/trigger click action, etc

When the main thread initiates an asynchronous task and then executes the rest of the code, the AJAX thread (asynchronous thread) executes the asynchronous task and listens for the completion of the task. If the asynchronous task gets a result (for example, the background returns a result and the timer counts down), The asynchronous thread stores the corresponding callback in the message queue with the result until it executes.

How do message queues and main threads work? Here’s a question for you to investigate: is it the task that calls the message queue when the main thread is idle, or is the message queue listening for the main thread to execute code, and if it is idle, pushing the main thread to execute code? Anyway, the bottom line is that a series of messages in the message queue will be executed in the main thread.

Since the main thread is executed, the above mentioned, as long as the time-consuming operation, put in the asynchronous queue will not solve the problem of page lag.

Timer:

The setTimeout function takes two arguments: the message to be queued and an optional time value (default: 0). This value represents the minimum delay for the message to actually be enqueued. If there are no other messages in the queue and the stack is empty, the message will be processed immediately after this delay has passed. However, if there are other messages, the setTimeout message must wait for the other messages to finish processing. So the second parameter only represents the minimum delay time, not the exact wait time.

Macro/micro tasks

We talked about event loops above, and as front-end developers we also know about macro/micro tasks. Yes, in the message queue, there is not just one task queue, but two task queues. The microtask queue is a VIP queue, which has a higher priority.

Let’s see how the main thread consumes these two queues.

Order of execution:

  • The main thread gets the task if it does not enter the wait
  • After a macro task is executed, all microtasks are executed
  • Then get the next microtask

whileGet task ()) {Execute task () microtask queue. ForEach (microtask => Execute microtask ())}Copy the code

So the question is, which are macro tasks and which are micro tasks? Macro/micro tasks are the two queue types of message queues, where micro tasks are created later to distinguish between higher priority tasks. The main thread clears the microtask queue each time before executing a macro task.

  • Macro tasks: normal asynchronous requests, timers, IO tasks, requestAnimationFrame
  • Microtasks: Promise, async/await, process.nextTick, queueMicrotask, MutationObserver

4. Interview questions

				function app() {
            setTimeout(() = > {
                console.log("1-1");
                Promise.resolve().then(() = > {
                	console.log("2-1");
                });
            });
            console.log("1-2");
            Promise.resolve().then(() = > {
                console.log("1-3");
                setTimeout(() = > {
                	console.log("3-1");
                });
            });
        }
        app();

/ / 1-2
/ / 1-3
/ / 1-1
/ / 2 to 1
/ / 3 to 1
Copy the code

Resolution:

  • Synchronize tasks first and print 1-2
  • The first layer of microtasks, print 1-3
  • Level 1 macro task, print 1-1
  • The second level of microtasks 2-1
  • Level 2 macro task 3-1

If you find this article helpful at all, give it a thumbs up and encouragement 😂.

Reference thanks

  • More on the Event Loop
  • Concurrency model and event loop
  • JavaScript asynchrony in detail
  • JavaScript single-threaded and asynchronous mechanisms
  • 🔥 “Punch the Interviewer” thoroughly understand event cycles, macro tasks, and micro tasks