浏览器的工作原理

从用户角度大访问一个网页大致是一个如下过程

image-20220820192409930

浏览器内核

不同浏览器有不同的内核。

  • Gecko:早期被 Netscape 和 Mozilla Firefox 浏览器浏览器使用;
  • Trident:微软开发,被 IE4~IE11 浏览器使用,但是 Edge 浏览器已经转向 Blink;
  • Webkit:苹果基于 KHTML 开发、开源的,用于 Safari,Google Chrome 之前也在使用;
  • Blink:是 Webkit 的一个分支,Google 开发,目前应用于 Google Chrome、Edge、Opera 等;

浏览器内核指的是浏览器排版引擎

  • 排版引擎(layout engine),也称为浏览器引擎(browser engine)、页面渲染引擎(rendering engine) 或 样板引擎

浏览器渲染过程

image-20220820192853813

上图是浏览器解析一个网页的大致过程。

  • 当 Html 网页被下载下来后,浏览器通过 HTML Parser 解析 html 结构 并将该结构转换成 DOM Tree,生成的 DOM Tree 可以使用 JS 代码进行操作。
  • 同样 HTML 所依赖的 CSS 相关内容 会被 CSS Parser 解析成 样式规则,然后将 样式规则 和 DOM Tree 结合在一起,然后通过布局引擎根据当前浏览器状态进行布局 最终生成 Render Tree
  • 最后 Painting 绘制 Render Tree,然后展示。

::: tip
javascript 代码是由谁来执行的呢?

  • JavaScript 引擎
    :::

JavaScript 引擎

为什么需要 JavaScript 引擎?

  • 高级的编程语言都需要转化成最终的机器指令来执行的。
  • 无论 JS 运行在 浏览器或 Node 上,最终都需要被 CPU 执行的。
  • 但是 CPU 只认识 自己的机器指令,也就是 01 代码又或者说机器语言。
  • 所以需要 JavaScript 引擎将 JavaScript 代码翻译成 CPU 指令来执行。

常见 JS 引擎

  • SpiderMonkey:第一款 JavaScript 引擎,由 Brendan Eich 开发(也就是 JavaScript 作者);
  • Chakra:微软开发,用于 IT 浏览器;
  • JavaScriptCore:WebKit 中的 JavaScript 引擎,Apple 公司开发;
  • V8:Google 开发的强大 JavaScript 引擎,也帮助 Chrome 从众多浏览器中脱颖而出;

浏览器内核和 JS 引擎的关系

以 WebKit 为例分析,WebKit 由如下两部分组成:

  • WebCore: 负责 HTML 解析、布局、渲染等相关的工作
  • JavaScriptCore : 解析、执行 JavaScript 代码

V8 引擎的原理

V8 引擎的定义:V8 是 使用 C++ 编写的 Google 开源高性能JavaScriptWebAssembly(浏览器的下一代语言)引擎,它用于ChromeNode.js等。它实现了 ECMAScriptWebAssembly,并且具有跨平台能力。同时可以独立运行或者嵌入到任何 C++ 应用程序中。

V8 引擎解析 Js 代码原理图

image-20220820195523611

  • Parse:解析过程后转化成抽象语法树(AST),因为解释器并不直接认识 JS 代码,如果函数没有被调用,那么是不会被转换成 AST 的

    • 词法分析
      • tokens: [{type:“keyword”, value: “const”} , {type: “identifier”, value: “name”}]
      • 这锅过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被叫做词法单元(token)
    • 语法分析
      • 将词法单元流(数组),转换成一个由元素逐级嵌套所组成的代表程序语法结构的树(AST树)
  • Ignition:

    • 将抽象语法树转换成 字节码(因为 JS 代码能够跑在不同的 CPU 架构上,不同的 CPU 架构的机器指令不尽相同,转换成字节码能够跨平台),最后 V8 引擎结合运行的平台转换成 对应的 机器指令 运行。
    • 同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)
  • TurboFan: 通过 Ignition 收集一些函数的执行信息, TurboFan 将使用频率高的函数标记成 热函数 ,并直接转换成机器指令,减少了字节码到机器指令转换的次数。

  • 过程 Deoptimization:

    • function sum(a, b) {
        return a + b;
      }
      
      sum(1, 2);
      sum(2, 3);
      // 上面执行的参数类型都相同,可以直接使用原来已经转换成的机器指令直接运行
      sum("aaa", "bbb");
      // 这个函数调用参数和之前的类型不同,所以之前已经转换成机器指令的函数已经不能应用在当前的函数调用上
      
    • 上面这种情况,v8 引擎会反向操作 Deoptimizaiont, 将机器码转化成字节码,然后将字节码再次转换成机器码再次运行。

所以这里推荐 函数的使用时的参数类型一定要固定,相比之下 TS 写出来的代码的效率应该比 JS 写出来的代码效率高。因为强制类型约束。

V8 官方图

image-20220820203646254

  • Blink: 将源码交给 V8 引擎,Stream 获取到源码并进行编码转换

  • Scanner: 会进行词法分析,词法分析会将代码转换成 tokens

  • tokens 会被转换成 AST 树,经过 Parser 和 PreParser:

    • Parser 就是直接将 tokens 转换成 AST 树

    • PreParser 称为预解析,为啥需要预解析?

      • 因为不是所有 js 代码,在一开始就会被执行。那么对所有 js 代码进行解析,必然会影响网页运行效率

      • 所以 v8 使用,Lazy Parsing (延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是暂时解析需要的内容,而对函数内的 全量解析 是在函数被调用时才会进行。

      • 比如我们在一个函数 outer 内部定义了另外一个函数 inner, 那么 inner 函数就会进行预解析

        • function outer() {
            function inner() {
              var msg = "Hello V8";
              console.log(msg);
            }
          }
          
          outer();
          
  • 生成 AST 树后,会被 Ignition 转换成字节码(bytecode), 之后过程就是代码执行的过程。

JS 执行过程

image-20220820204839323

  • V8 在解析阶段会生成一个 全局对象 GlobalObject(GO 放在堆内存里面,所有 scope 都可以访问), 全局对象包含全局中能够直接使用的标识符,和自定义的标识符。

    • 例如:

      • var a = 1;
        var b = 2;
        var name = "acwink";
        
        function foo() {
        
        }
        
        // 在解析阶段会生成GlobalObject对象的伪代码如下
        const globalObject = {
            String,
            Date,
            setTimeout,
            ... 等能够不用定义直接使用的函数变量和其他的
            // 自定义的变量或函数
            a: undefined,
            b: undefined,
            name: undefined,
        	foo: 0x001
        }
        
        
      • 因为此时处于解析阶段,代码还没有真正的运行,所以所有变量的值都是 undefined。当然 V8 争对函数有特殊的处理(后面会写到)

  • AST 树就会通过 Ignition 解析成字节码,字节码转化成机器指令开始执行。

  • 为了执行代码 V8 内部会有 执行上下文栈(Execution Context Stack)(函数调用栈),为了执行全局代码,会创建 全局执行执行上下文(GEC (global exection context))GEC 会被加入到 ECS.

    • 全局执行上下文内容如下
      • image-20220820210538326
  • 上面所有东西准备好后开始执行代码

  • 而第一行代码 name = "acwink", 中的 name 会在 VO 对象中去找,也就会找到 GO 对象中的 name 属性,并将它赋值成 "acwink", 其表达式同理(函数除外,函数是一等公民),这里就包含了作用域提升的概念