An article to understand the working principle of the browser (detailed explanation of 10,000 words)

Summary

This article is about studying the course on Geek Time and then sorting out the working principles of the browser.

Part One: Browser Processes and Threads

(1) What is the difference between process and thread?

In the browser, each process is responsible for handling its own affairs, and in different processes, there are threads that cooperate with each other. Therefore, before understanding the working principle of the browser, you must understand the difference between processes and threads.

  1. A thread cannot exist alone, it must be started and managed by a process
  2. A process is an instance of a program
  3. Threads are attached to processes. Using multiple threads in a process can improve efficiency.
  4. Sharing process data between threads
  5. When a process is shut down, the operating system will reclaim the memory occupied by the process.
  6. Processes are independent of each other

(2) Disadvantages of single-process browsers

In ancient times, browsers were single-process. Of course it is definitely not the case now. The so-called single-process browser means that the entire browser works in one process. So what are the shortcomings of a single-process browser?

  1. Unstable: Whether there is a problem with a page, a JS program, a script or a plug-in, it will cause the entire browser to crash, directly GG.
  2. Unsmooth: All page rendering is in one process. If the efficiency of a rendering JS is relatively low, it will cause the entire rendering process to be unsmooth. At the same time, rendering in a single process is too complex, and the browser cannot reclaim all memory after closing the page, resulting in memory leaks.
  3. Unsafe: Custom scripts and plug-ins will obtain system permissions and security data of the operating system

(3) Multi-process browser

Since the shortcomings of single-process browsers are so obvious, can we consider using a single process with a relatively heavy load? What about splitting it into multiple processes responsible for different functional modules?

Insert image trace here
As you can see in the picture above, different processes are responsible for different content.

Work content of different processes

Browser main process: Mainly responsible for page display, user interaction, sub-process management, and providing storage functions.

Rendering process: Each page has its own rendering process. It mainly converts html, js, and css into web pages that can be displayed. The V8 engine we often hear about is in this process.

GPU process: Mainly to achieve 3D and CSS effects

Network process: Mainly responsible for network loading

Plug-in process: If there is a plug-in, a separate process will be opened for the plug-in to run.

So we now know, if someone asks you
When you open a browser, how many processes does the browser open? The answer is four, because it is possible that there is no plug-in process.

Of course, you can also open the browser's task manager to view the current browser process yourself:

Insert image description here

Disadvantages of multiple processes

Of course, the disadvantages seem insignificant compared to the advantages.

  1. Higher resource usage: Because there are more processes, more resources will be taken up.
  2. More complex architecture: Poor browser coupling and poor scalability will make it difficult for the current architecture to adapt to new needs.

(3) Frequently Asked Questions

Question: Since each page has a rendering process, why does it sometimes affect other pages when one page gets stuck?

Answer: Generally speaking, there is only one rendering process for a page. But if two pages come from the same site (for example, just different port numbers), then the two pages will use the same rendering process.

Q: How does multi-process solve security issues.

Answer: Sandbox processing is done in the rendering process and plug-in process. They cannot write any data to the hard drive, nor can they read data in sensitive locations.

Part 2: From entering the URL to page display, what happens in between?

OK, this is also a very common interview question. Now, we understand what each process does. We explain this problem from the perspective of process.

Insert image description here
Attach the flow chart first, and you can compare it with this picture in the subsequent explanation.

browser process

  • Determine whether the keyword entered by the user is search content or URL
  • If you are searching for content, use the browser's default search engine to synthesize a URL with the search content.
  • If it is the requested URL, the requested URL will be spliced ​​into a legal and complete URL, and the requested URL will be handed over to the network process through IPC.

network process

  • The network process searches the local cache to see if it hits the strong cache. If it hits, the cache is directly handed over to the browser process.
  • If the strong cache is not hit, the domain name is resolved into an IP address through DNS.
  • Use the IP address to establish a TCP connection, send an HTTP request, and the server returns response information.
  • In the returned information, if the negotiation cache is hit, continue to obtain resources from the cache and hand them over to the browser process. Otherwise proceed to the next step
  • If the returned status code is 301 or 302, you need to redirect and go back to the first step.
  • If the returned status code is 200, then the response data is read and handed to the browser process.
  • Close TCP connection

If you are unfamiliar with HTTP caching, you can read this article first to learn about strong caching and negotiated caching:
Http cache - strong caching and negotiated caching (detailed explanation)< /span>

rendering process

  • At this point, the rendering process has been prepared, but the document information is still in the browser process.
  • Information about the browser process initiating document submission
  • After the rendering process receives the message of submitting the document, it establishes a transmission pipeline with the network process.
  • After the data transmission is completed, the rendering process returns confirmation submission information to the browser process.
  • After the browser process receives the message, it will update the status of the browser (the URL of the page, the historical status of forward and backward), and update the page.

So next time if someone asks you this question, you can explain it from the perspective of browser multi-process.

Part 3: Browser Rendering Process

(1) Build DOM tree

When the rendering process obtains the requested resource, the HTML document cannot be recognized by the browser. Therefore, it is necessary to convert the html document into a DOM tree that can be recognized by the browser.
The DOM tree is a tree structure with a hierarchical relationship, and each node above is a DOM element.

The DOM tree is also a document, which exposes many APIs for operating nodes, such as the document.getElementById method. We can enter document in the browser console to view the DOM tree.

Insert image description here

(2) Style calculation

In this process, the main thing is to calculate the specific style corresponding to each DOM node.

Transform CSS structure

There are three main sources of CSS:

  1. External CSS introduced through the link tag
  2. CSS content in style tag
  3. Nested CSS styles within elements

The browser cannot understand CSS styles, so it needs to be converted into a structure that the browser can understand, styleSheets.
You can view the structure of the CSSOM tree through document.styleSheets.

Insert image description here

Convert stylesheet attribute values ​​to standardize them

Because the attribute values ​​we write in CSS are all various, we need to convert them into standard values ​​that are easily recognized by the browser.

Insert image description here

Calculate the style of each node in the DOM tree

Because in CSS, we know that there is a very important mechanism, which is inheritance. The inherited styles are not marked on the corresponding DOM nodes, so we need to calculate the style of each DOM node in this process.

Insert image description here

(3) Layout stage

After the browser converts html and css into DOM trees and CSSOM trees, it cannot render the page. It needs to go through the process of layout calculation.

Create layout tree

This process mainly checks the style of each node on the DOM tree. Whether it has a hidden style (such as display:none), if so, block it to build a new layout tree.

Insert image description here

layout calculation

This link mainly calculates the coordinate position of each node in the layout tree.

(4) Stratification stage

After having the layout tree, the page still cannot be rendered. Because the Z-axis and 3D animation issues were not taken into account. So in this step, the layout tree needs to be layered.

The so-called hierarchical processing is to divide the layout tree into several layers, with each node in its own layer.

Insert image description here
Not every node has its own layer, a node will have its own layer created individually only if the following two conditions are met. Otherwise, it belongs to the layer of the parent node.

(1) Elements with cascading context

Insert image description here
(2) Layers will be created where areas need to be cropped.

For example, if the content area is larger than the parent container, a separate layer will be created for the extra content.

Finally, it goes through layer drawing, rasterization operation, composition and display. The work of the rendering process is completed (if you are interested in the following three steps, you can check the information yourself)

(5) Redraw, rearrange, synthesize

These three concepts also often appear in interviews. Then if you already understand how the rendering process works, you can explain this problem from this aspect.

(1) Heavy exclusion
Insert image description here
(2) Heavy exclusion
Insert image description here

(3) Synthesis
Insert image description here

Part 4: Is JS executed sequentially?

When we have a piece of JS code, how is JS executed?
JS is compiled first and then executed. For example:

showName()
var showName = function() {
    
    
    console.log(2)
}
function showName() {
    
    
    console.log(1)
}
showName()

Looking at this code, what should be output? There must be many people who have encountered similar problems during interviews.

For example, in the above code, JS must be compiled first, find the variables defined by var, let, const, and function, and define them. Remember,the assignment operation is not executed at this stage. After the above code is compiled:

var showName
function showName(){
    
    console.log(1)}

Then execute:

showName()//输出1
showName=function(){
    
    console.log(2)}
showName()//输出2

So it will output 1 first, and then 2.

Part 5: JS call stack

Presumably many people have encountered JS function scope and other related issues during interviews. This section will explain how the execution of JS creates a context.

Suppose I have the following code:

var a = 2
function add(b,c){
    
    
  return b+c
}
function addAll(b,c){
    
    
var d = 10
result = add(b,c)
return  a+result+d
}
addAll(3,6)

Now we know that a piece of JS code must be compiled first and then executed.

(1) Compilation and execution process

Step one: Create a global context and push the compiled content to the bottom of the stack.

Insert image description here

Step 2: Execute the code, a = 2;

Insert image description here

Step 3: Call the addAll function, compile the content in the function, create a new execution context, and push it onto the stack.

Insert image description here

Step 4: Execute the code in the addAll function, d=10;
Step 5: Call the add function, continue to compile the content in the add function, create a context, and push it onto the stack.

Insert image description here

Step 6: Execute the code of the add function. After execution, the add execution context is popped from the stack.

Insert image description here

Step 7: Return the result to the addAll execution context and execute the return statement. After the code in the addAll context is executed, it pops up.

Insert image description here
Then the entire JS code is executed and ends.

(2) Frequently Asked Questions

Q: How to view the call stack in the browser?

You can view it in the call back on the right. It can also be viewed through the console.trace() method.

Insert image description here
Question: What is stack overflow?

If there are too many function execution contexts in the call stack and reach a critical value, a stack overflow error will be reported. You can write your own code to try out what the browser's critical value is.

Part 6: The difference between let and var

This problem must be easy to grasp, right? What variable is improved cannot be defined repeatedly. .
But we can also explain the difference between the two from the execution context.

In the flow chart above, you will find that there is a lexical environment that has not been used. And it appears specifically for variables defined by let and const.

For example, I now have this code:

function foo(){
    
    
    var a = 1
    let b = 2
    {
    
    
      let b = 3
      var c = 4
      let d = 5
      console.log(a)
      console.log(b)
    }
    console.log(b) 
    console.log(c)
    console.log(d)
}   
foo()

There is only one function foo, so after compilation, this code should be:
Insert image description here
Variables defined by var are placed in the variable environment, and variables defined by let are placed in the variable environment. Lexical environment. OK, after compilation is completed, execute the code.
a = 1; b = 2;
Execute block-level scope and continue compilation:
Insert image description here

A c is added to the variable environment, and a b and d are added to the lexical environment. And maintain a stack structure in the lexical environment.

Execute code inside block-level scope:
Insert image description here

However, the above process is for ease of understanding. The real block-level scope will not be compiled. The compilation process will only exist for the first execution, so the top-level data of the variable environment and lexical environment have been determined at compile time.

When executing to the block-level scope, variables declared through let and const in the block-level scope will be appended to the lexical scope. When the block execution ends, the content appended to the lexical scope will be destroyed.

Part 7: Scope and Scope Chain

Let’s look at a piece of code first:

function bar() {
    
    
    console.log(myName)
}
function foo() {
    
    
    var myName = "极客邦"
    bar()
}
var myName = "极客时间"
foo()

Please answer what is the result of executing this code. Now let's explain it through scope.

Now we can clearly write out the entire execution stack:
Insert image description here
When the myName variable cannot be found in the execution context of bar. It searches along the scope chain.
The scope chain is determined when the code is defined, not at compile time.

Each execution context has an auto pointer, pointing to the previous context on the scope chain:
Insert image description here
The scope chain is determined by the lexical scope. Lexical scope is a static scope, determined by code position:
Insert image description here

closure case

Let’s look at a piece of code:

function foo() {
    
    
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = {
    
    
        getName:function(){
    
    
            console.log(test1)
            return myName
        },
        setName:function(newName){
    
    
            myName = newName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())

Before looking at the following content, you can try to write out the printing results first.

Now let's analyze the whole process step by step.

(1) First, create the global execution context and create the execution context of the foo function:
Insert image description here
(2) After compilation, execute the foo function and assign innerBar to bar. After the foo function is executed, it will pop up from the call stack, but because the variables in the foo function are accessed in innerBar.

So even if the foo function pops up, getName and setName can still access myName and test1.
Insert image description here
So let’s give closure a unique definition:

In JavaScript, according to the rules of lexical scope, internal functions can always access variables declared in their external functions. When an internal function is returned by calling an external function, even if the external function has finished executing, the internal function reference The variables of the external function are still stored in memory, and we call the collection of these variables a closure. For example, if the external function is foo, then the set of these variables is called the closure of the foo function.

Part 8: Garbage collection mechanism

Another common question in interviews:
First of all, we know that in JS, the memory of variables is stored in the call stack. The memory of the object is stored in the heap memory.
If you don’t understand heap memory and stack memory, you can read related articles first.
For example, I have this piece of code:

function foo(){
    
    
    var a = 1
    var b = {
    
    name:"极客邦"}
    function showName(){
    
    
      var c = 2
      var d = {
    
    name:"极客时间"}
    }
    showName()
}
foo()

The space status of the call stack and heap memory is as shown in the figure:

Insert image description here
When the showName function is executed, the contexts in the stack will pop up one by one, but the data in the heap memory has not been cleared yet. So the garbage collection mechanism we are talking about mainly targets the data in the heap memory.

(1) Intergenerational hypothesis

The intergenerational hypothesis has two characteristics:

  • Most objects exist in memory for a short time
  • Immortal objects will live for a long time

Based on this feature, V8 divides memory into two types. Young generation memory and old generation memory. For these two parts, V8 uses different mechanisms to implement garbage collection.

  • Deputy garbage collector: responsible for recycling the memory of the new generation
  • Main garbage collector: responsible for recycling the memory of the old generation

(2) Deputy garbage collector

is mainly processed using the Scavenge algorithm in the new generation memory. The so-called Scavenge algorithm divides the memory of the new generation into two parts, one part is the object area , and the other part is free Area.

Insert image description here
Every time a new object is added, it will be added in the object area. When the object area is full, a garbage collection operation will be performed.

Mark all the memory in the object area. After the marking is completed, the surviving objects will be copied to the free area, and these objects will also be arranged in an orderly manner. Therefore, the copying process is also a process of memory organization.

Then the object area and the free area are exchanged, and when the new object area is full again, the garbage collection operation is performed again.

Note: V8 has another strategy:If it still survives in the object area after two garbage collections, it will be moved to the old generation area.

(3) Main garbage collector

The main garbage collector mainly performs garbage collection throughmark-clearance.

mark stage

By traversing the call stack, if there is a reference to the memory, it will be marked as a live object, if not, it will be marked as garbage data.

clearing phase

Clear memory of junk data
Insert image description here

However, after performing the mark and clear algorithm on an area, a large number of discontinuous fragments will be generated. Since discontinuity will prevent large objects from being allocated to contiguous memory, another algorithm was developed, mark-organize.

The marking method is still the same, except that all surviving objects are moved to a section at the same time. After the move is completed, all data outside the boundary will be cleared.

Insert image description here

full pause

Since JS is executed on the main thread, once the garbage collection algorithm is executed, the JS on the main thread will stop and continue execution after the garbage collection is completed. This situation is called a full pause.

Insert image description here
In order to reduce the lag caused by old generation garbage collection, the V8 engine divides the marking process into a sub-process, and at the same time allows the garbage collection operation and JS to run alternately until the marking phase is completed. We call this algorithm forincrement-mark.

Insert image description here

Part 9: Event Loop

The event loop is also a common question in interviews.
The event loop of JS is implemented through the browser’s message queue:
Insert image description here

  • Rendering process: used to parse JS code
  • Message queue: used to store macro task lists
  • IO thread: used to communicate with other threads

Each macrotask has a microtask queue, which is used to store microtasks. When all the codes in the macro task are executed, the micro tasks in the micro task queue will be executed.

Now I use a piece of code to describe:

 console.log(123)
 setTimeout(() => {
    
    
   console.log('time');
   new Promise((resolve,reject) =>{
    
    resolve()}).then(() => {
    
    
       console.log(123);
   })
 }, 0);
 new Promise((resolve,reject) =>{
    
    resolve()}).then(() => {
    
    
   console.log(123);
 })
 console.log(789);
  1. Execute the code and create macro task H1 for the overall JS code.
  2. Execute macro task H1 and print 123
  3. When setTimeout is encountered, create macro task H2 and continue executing the code
  4. When Promise.then is encountered, create microtask queue W1 in macrotask H1 and put Promise.then in the queue.
  5. Continue to execute the code and output 789
  6. After the macro task H1 code is executed, the code in the micro task queue W1 starts to be executed, and 123 is output.
  7. After the microtask queue in macrotask H1 is executed, macrotask H2 begins to execute.
  8. Output time, encounter Promise.then, add microtask queue W2 to macrotask H2
  9. Execute the code in W2 and output 123

OK, so the event loop of this code is clearly described.
Insert image description here

Part 10: await and async

The event loop was mentioned above, but we know that in JS, await and async are also related to asynchronous.
Before explaining await and async, let's first talk about the concepts of Generator and coroutine.

Look at the following code:
If you don’t understand Generator, you can take a look at the related concepts first.

function* genDemo() {
    
    
    console.log("开始执行第一段")
    yield 'generator 2'

    console.log("开始执行第二段")
    yield 'generator 2'

    console.log("开始执行第三段")
    yield 'generator 2'

    console.log("执行结束")
    return 'generator 2'
}

console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')

In the rendering process, there is a main thread used to execute code, the message queue is used to store macro tasks, and the IO thread is used to process and accept external events.

The concept of coroutine is to open another path for execution in the main thread.
The execution of the above code is:

  • let gen = genDemo(): open a coroutine
  • gen.next(): Gives the execution initiative to the gen coroutine
  • yield: Return the execution initiative to the main thread
  • return: close the coroutine

Insert image description here
OK. If you understand Generator, let's take a look at await:

async function foo() {
    
    
    console.log(1)
    let a = await 100
    console.log(a)
    console.log(2)
}
console.log(0)
foo()
console.log(3)

For this code:

  • Execute the foo function and open the foo coroutine
  • Execute to await 100, at which time await will create a promise object
  • The promise object is handed over to the main thread, and the main thread monitors changes in the foo coroutine through Promise.then(). In fact, it creates a microtask queue to store the remaining content in the foo coroutine.
  • Execute the code of the main thread. After execution, execute the contents of the microtask queue.

Part 11: How JS affects the construction of the DOM tree

(1) How the DOM tree is constructed

  • The network process receives the response and determines whether the content-type is text/html. If so,
  • Create a rendering process and establish a pipeline with the network process
  • The network process delivers the requested content to the rendering process through a pipeline

In the rendering process, there is an HTML parser specifically used to parse documents. The parsing process is as follows:
Insert image description here

Convert byte stream into Token through tokenizer

Token is divided into Tag Token (corresponding to div, span, etc.) and Text Token< /span> (corresponds to the end of the tag). EndTag (corresponding to the beginning of the tag) and StartTag
Tag Token is divided into (corresponds to text node).

Insert image description here

Calculate the parent-child relationship of nodes

After converting HTML to the corresponding Token, it is necessary to find out the relationship between Token and Token.

In the HTML parser, a Token stack structure is maintained. This stack is mainly used to calculate the corresponding parent-child relationship between nodes. The tokens generated in the first phase will be pushed onto the stack in order.

  • If a StateTag is pushed onto the stack, the HTML parser will create a DOM node for the Token and insert the node into the DOM tree. The parent node of this node is the DOM node corresponding to the Token adjacent to the Token.
  • If it is a TextToken, the Token will not be pushed onto the stack, but a text node will be directly generated and inserted into the DOM tree. The corresponding parent node is the top element of the stack.
  • If the EndTag is pushed, it will check whether the top element of the stack is a StartTag. If so, the top element of the stack will be popped, indicating that the Token has been parsed.

(2) How JS affects DOM generation

Let’s take code as an example:

JS script
<html>
<body>
    <div>1</div>
    <script>
    let div1 = document.getElementsByTagName('div')[0]
    div1.innerText = 'time.geekbang'
    </script>
    <div>test</div>
</body>
</html>

For this code, when the HTML parser encounters the script tag, the HTML parser will pause. Because the execution of JS may affect the DOM. So you have to wait for JS to be executed before parsing.

Dynamic introduction
<html>
<body>
    <div>1</div>
    <script type="text/javascript" src='foo.js'></script>
    <div>test</div>
</body>
</html>

Through the introduction of src, the HTML parser will still suspend its work, but before executing the JS code, the JS file must be downloaded first, and The download of JS is very Time consuming.

So Google Chrome has optimized this aspect, which is called pre-parsing operation. The rendering process analyzes the JS and CSS files in the HTML and downloads them in advance.

So if it is not a JS script that modifies DOM operations, you can use the defer attribute or the async attribute to execute the script asynchronously.
The difference between the two is:

  • The async script is executed immediately after loading, while the defer script is executed before the DOMContentLoaded event.
  • Scripts marked with defer need to be executed in order, while async is executed after loading.

(3) CSS blocking

<html>
    <head>
        <style src='theme.css'></style>
    </head>
<body>
    <div>1</div>
    <script>
            let div1 = document.getElementsByTagName('div')[0]
            div1.innerText = 'time.geekbang' //需要DOM
            div1.style.color = 'red'  //需要CSSOM
        </script>
    <div>test</div>
</body>
</html>

Normally, when rendering a page, the DOM tree should be built first and then the style calculation is performed.

But if we modify the DOM style in JS, we must first parse the CSS style of the corresponding node. If it is imported from outside, then it must be downloaded first.

So:
CSS will not block the generation of DOM, but it will block the execution of JS, thus affecting the construction of the DOM tree

OK, there is still some content at the end of the course. I can’t write it anymore. If I don’t want to write it anymore, I’ll just write it here.

Guess you like

Origin blog.csdn.net/weixin_46726346/article/details/132446223