Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to understand the browser event loop

2025-01-18 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/02 Report--

This article introduces the knowledge of "how to understand the browser event cycle". In the operation of actual cases, many people will encounter such a dilemma, so let the editor lead you to learn how to deal with these situations. I hope you can read it carefully and be able to achieve something!

Why should there be an event loop?

JS engine

To answer this question, let's first look at a simple example:

Function c () {} function b () {c ();} function a () {b ();} a ()

How on earth does the above simple JS code be executed by the browser?

First, if the browser wants to execute the JS script, it needs a "thing" to turn the JS script (essentially a plain text) into a piece of computer instructions that the machine can understand and execute. This "thing" is the JS engine, which actually compiles and executes JS scripts. The whole process is very complex. I will not introduce you too much here. If you are interested, you can look forward to my V8 chapter. If there is no special description, take V8 as an example.

There are two very core components, the execution stack and the heap. The execution stack holds the executing code, and the value of the variable in the heap is usually irregular.

When V8 executes to the line a (), an is pushed to the top of the stack.

Inside a, we encounter b (), which is when b is pushed into the top of the stack.

Inside b, we encounter c () again, when c is pushed into the top of the stack.

C after execution, it will be removed from the top of the stack.

The function is returned to BBM and b is finished, and b is removed from the top of the stack.

A will also be removed.

The whole process is animated like this:

(watch online)

At this time, we have not covered heap memory and execution context stack, everything is relatively simple, we will talk about these later.

DOM and WEB API

Now we have an engine that can execute JS, but our goal is to build the user interface, while the traditional front-end user interface is based on DOM, so we need to introduce DOM. DOM is a document object model that provides a series of interfaces that JS can call directly. In theory, it can provide interfaces for other languages, not just JS. And in addition to the DOM interface that can be called to JS, the browser also provides some WEB API. Whether DOM or WEB API, it has nothing to do with JS in essence. It's totally different. The ECMA specification corresponding to JS, V8 is used to implement the ECMA specification, regardless of the rest. This is also the difference between the JS engine and the JS execution environment, V8 is the JS engine, which is used to execute JS code, and the browser and Node are the JS execution environment, which provides some API that can be called by JS, namely JS bindings.

Thanks to browsers, JS can now manipulate DOM and WEB API, and it looks like you can build a user interface. One thing to be clear in advance is that V8 only has stacks and heaps, and DOM,WEB API knows nothing about other things such as event loops. The reason has actually been mentioned earlier, because V8 is only responsible for the compilation and execution of JS code, you give V8 a piece of JS code, it will be executed from beginning to end, and it will not stop.

In addition, I would like to continue to mention that the JS execution stack and the rendering thread block each other. Why? In essence, because JS is too flexible, it can get information such as coordinates in DOM. If the two are executed at the same time, there may be a conflict, for example, I first get the x coordinates of a DOM node, and the next moment the coordinates change. JS uses this "old" coordinate to calculate and assign a value to DOM, and a conflict occurs. There are two ways to resolve conflicts:

Hongmeng official Strategic Cooperation to build HarmonyOS Technology Community

Limit the ability of JS, you can only use certain API at certain times. This practice is extremely complex and will bring a lot of inconvenience.

It would be nice if the JS and the rendering thread do not execute at the same time, and one way is to block each other, which is now widely used. In fact, this is also a method widely used by browsers at present.

Single-thread or multi-thread or async

As mentioned earlier, if you give V8 a piece of JS code, it will be executed in one breath from beginning to end without stopping in the middle. Why not stop, can it be designed to be stoppable, just like C language?

Suppose we need to get user information, get users' articles, and get friends.

Single thread without async

Since it is single-threaded, our three interfaces need to be synchronized.

FetchUserInfoSync () .then (doSomethingA); / / 1s fetchMyArcticlesSync () .then (doSomethingB); / / 3s fetchMyFriendsSync () .then (doSomethingC); / / 2s

Because the above three requests are executed synchronously, the above code executes fetchUserInfoSync first, fetchMyArcticlesSync one second later, and fetchMyFriendsSync three seconds later. The most frightening thing is that we just said that the JS execution stack and the rendering thread are blocking each other. Therefore, during this period, the user cannot operate at all, and the interface cannot respond, which is obviously unacceptable.

Multithreading without asynchronism

Because it is multi-threaded, although our three interfaces still need to be synchronized, we can execute the code in multiple threads, for example, we will execute this code in three threads.

Thread one:

FetchUserInfoSync () .then (doSomethingA); / / 1s

Thread two:

FetchMyArcticlesSync () .then (doSomethingB); / / 3s

Thread 3:

FetchMyFriendsSync () .then (doSomethingC); / / 2s

Because three pieces of code are executed at the same time, the total time is ideally determined by the slowest time, that is, 3s, which is the same as using asynchrony (provided, of course, there are no dependencies between requests). Why do you say the ideal? Because all three threads have access to DOM and heap memory, it is likely to conflict, and the cause of the conflict is not fundamentally different from the conflict between the JS thread and the rendering thread I mentioned above. So ideally, if there is no conflict, it is 3s, but if there is a conflict, we need to resolve it with the help of locks, so that the time may be longer than 3s. Accordingly, the programming model will be more complex, and programmers who have dealt with locks should feel the same way.

Single thread + async

Wouldn't it be better to change to asynchronous if you still use single threading? The crux of the problem is how to achieve asynchrony. That's the theme we're going to talk about-the event cycle.

How on earth is the event loop asynchronous?

We know that there is only one JS thread in the browser, and if there is no event loop, it will cause a problem. That is, if JS initiates an asynchronous IO request, the rest of the code will be blocked while waiting for the result to return. We know that the JS main thread and the rendering process block each other, so this will cause the browser to fake death. How to solve this problem? One effective way is the event cycle that we will talk about in this section.

In fact, event loops are used for scheduling, and events in browsers and NodeJS go bad just like operating system schedulers. The scheduler of the operating system decides when and what resources are allocated to whom. For computers with threading model, then the smallest unit of operating system code execution is the thread, the smallest unit of resource allocation is the process, and the process of code execution is scheduled by the operating system, and the whole scheduling process is very complex. We know that many computers are multicore now, in order to make multiple core work at the same time, that is, no core is particularly idle, and no core is particularly tired. The operating system's scheduler performs some mysterious algorithm to ensure that each core can be assigned a task. This is why when we use NodeJS for clustering, the number of Worker nodes is usually set to the number of core. The scheduler will try to allocate each Worker to each core evenly. Of course, this process is not certain, that is, the scheduler is not necessarily allocated in this way, but it happens in many cases.

Now that we know how the operating system scheduler works, we might as well go back to the event loop. The event loop is essentially scheduled, except that the scheduled object becomes the execution of the JS. The event loop determines when and what code V8 executes. V8 is only responsible for parsing and executing JS code, and it knows nothing else. After the event is triggered in the browser or NodeJS, all the work performed by V8 during this period of time for the listener function to the event is the event loop.

Let's make a summary:

For V8, it has:

Call stack (call stack)

Single threading here means that there is only one call stack. Having only one call stack means that only one piece of code can be executed at a time.

Heap (heap)

For the browser operating environment:

WEB API

DOM API

Task queue

Event to trigger the event loop to flow

Take the following code as an example:

Function c () {} function b () {c ();} function a () {setTimeout (b, 2000)} a ()

The execution process goes like this:

(watch online)

So the event loop can be asynchronous because when it comes to asynchronously executed code such as fetch,setTimeout, the browser saves the callback function registered by the user and continues to execute the following code. At some point in the future, when the "asynchronous task" completes, an event is triggered, and the browser passes the "task details" as an argument to the callback function bound by the user. Specifically, the callback function bound by the user is pushed into the browser's execution stack.

But it does not mean that it is pushed randomly. Only when the browser executes the JS script that of course needs to be executed in one breath will it check to see if there are any "messages" to be processed.

If so, the callback function bound to the corresponding message is pushed onto the stack. Of course, if there is no binding event, the event message will actually be discarded and not processed. For example, if the user triggers a click event, but the user does not bind a listener for the click event, the event will actually be discarded.

Let's take a look at what it looks like after joining the user interaction, taking the click event as an example:

$.on ('button',' click', function onClick () {setTimeout (function timer () {console.log ('You clicked the responsibility');}, 2000);}); console.log ("Hi!"); setTimeout (function timeout () {console.log ("Click the button!");}, 5000); console.log ("Welcome to loupe.")

Each time the above code clicks the button, it sends an event because we bind a listener function. Therefore, each click, there will be a click event message, the browser will be "idle" corresponding to the user-bound event handler pushed to the stack for execution.

Pseudo code:

While (true) {if (queue.length > 0) {queue.processNextMessage ()}}

Animated demonstration:

(watch online)

Join Macro Task & Micro Task

Let's look at a more duplicated example and feel it.

Console.log (1) setTimeout (() = > {console.log (2)}, 0) Promise.resolve (). Then (() = > {return console.log (3)}). Then (() = > {console.log (4)}) console.log (5)

The above code will output: 1, 5, 3, 4, 2. If you want a very rigorous explanation, you can refer to whatwg's description of it-event-loop-processing-model.

I will give a brief explanation to it below.

The browser first executes the macro task, that is, we script (only once)

After completion, check to see if there are microtasks, and then keep executing until the queue is cleared

Perform macro tasks

Where:

Macro tasks mainly include: setTimeout, setInterval, setImmediate, Imax O, UI interactive events

Micro tasks mainly include: Promise, process.nextTick, MutaionObserver, etc.

With this knowledge, it is not difficult for us to get the output of the above code.

From this we can see that the macro task-micro task is only in the asynchronous process, we have a different order of signal processing. If we make no distinction and put them all in one queue, there will be no macro tasks & micro tasks. This artificial prioritization process is very useful at some point.

Join the execution context stack

Speaking of the execution context, we have to mention that the browser executes the JS function in two processes. One is the creation phase Creation Phase, and the other is the execution phase Execution Phase.

Like the execution stack, every time the browser encounters a function, it pushes the execution context stack of the current function to the top of the stack.

For example:

Function a (num) {function b (num) {function c (num) {const n = 3 console.log (num + n)} c (num);} b (num);} a (1)

Encounter the above code. First, the execution context of an is pushed into the stack, and we begin the creation phase Creation Phase, pushing the execution context of an into the stack. Then initialize the execution context of a, which is VO,ScopeChain (VO chain) and This, respectively. We can also see from here that this is actually determined dynamically. VO refers to variables, functions and arguments. And the execution context stack is also destroyed synchronously with the destruction of the execution stack.

The pseudo code indicates:

Const EC = {'scopeChain': {},' variableObject': {}, 'this': {}}

Let's focus on ScopeChain (VO chain). As the execution context in the above figure looks like this, pseudo code:

Global.VO = {a: pointer to a (), scopeChain: [global.VO]} a.VO = {b: pointer to b (), arguments: {0: 1}, scopeChain: [a.VO, global.VO]} b.VO = {c: pointer to c (), arguments: {0: 1}, scopeChain: [b.VO A.VO, global.VO]} c.VO = {arguments: {0: 1}, n: 3 scopeChain: [c.VO, b.VO, a.VO, global.VO]}

When looking for variables, the engine will start with VOC. If it can't find it, it will continue to VOB..., until GlobalVO. If GlobalVO can't find it, it will return Referrence Error. The whole process is similar to the search of prototype chain.

It is worth mentioning that JS is lexical scope, that is, static scope. In other words, the scope depends on where the code is defined, not where it is executed, which is the essential reason for closures. If the above code is modified to the following:

Function c () {} function b () {} function a () {} a () b () c ()

Or this:

Function c () {} function b () {c ();} function a () {b ();} a ()

Although the execution context stack is all the same, the corresponding scopeChain is completely different because the location of the function definition has changed. Taking the code snippet above, c.VO would look like this:

C.VO = {scopeChain: [c.VO, global.VO]}

That means it can no longer get the VO in an and b.

That's all for "how to understand the browser event loop". Thank you for reading. If you want to know more about the industry, you can follow the website, the editor will output more high-quality practical articles for you!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report