01_浏览器工作原理和 V8 引擎

JavaScript 是一门编程语言

首先我们要明确 JavaScript 是一门高级的编程语言!

除此之外还有汇编语言,再往下就是机器语言,计算机实际能够读懂的就是机器语言,它就是一些机器指令(010101~)。

越是高级的语言往往越抽象(机器层面),越容易被人所理解,反之,则越容易被机器解读,越不容易被人所理解。

浏览器的工作原理

那么,我们的 JavaScript 代码是如何被我们的浏览器所执行的呢?回答这个问题需要先理解以下几个概念:

1. 浏览器内核

不同浏览器有不同的内核组成:

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

实际上,我们所说的浏览器内核指的是浏览器的排版引擎:

早期浏览器内核实际上应该指的是渲染引擎(排版引擎...)和 JS 引擎,但是随着 JS 引擎越来越独立,内核就倾向于单指渲染引擎。

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

2. JavaScript 引擎

为什么需要 JavaScript 引擎呢?

  • JavaScript 是一门高级的编程语言,机器本身是不可读的,需要一个转换到机器码的过程;
  • 事实上我们编写的 JavaScript 无论你交给浏览器或者 Node 执行,最后都是需要被 CPU 执行的;
  • 但是 CPU 只认识自己的指令集,实际上是机器语言,才能被 CPU 所执行;
  • 所以我们需要 JavaScript 引擎帮助我们将 JavaScript 代码翻译成 CPU 指令来执行;

常见的 JavaScript 引擎:

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

3. 浏览器内核和 JavaScript 引擎的关系

这里我们以 Webkit 为例,Webkit 实际上是由两部分组成的:

  1. WebCore:负责解析、渲染、布局等工作;
  2. JavaScriptCore: 解析和执行 JavaScript 代码;

实际上,这和我们小程序的架构非常类似:

  • 小程序中我们的 JavaScript 代码就是 JavaScriptCore 负责执行的

小程序架构图

另外一个非常强大的引擎就是我们经常听到的 V8 引擎。

4. 浏览器的渲染过程

浏览器的渲染过程

上图是一个浏览器解析我们文件的具体过程:

  1. 首先浏览器拿到我们的文件,从上往下执行:
    1. 如果是样式文件,交给 CSS Parser 处理解析
    2. 如果是 HTML 标签,交由 HTML Parser 处理解析
  2. 由于我们的 JavaScript 文件可以操纵 DOM 元素,所以势必会影响到 HTML 解析生成 DOM Tree 的过程,所以如果解析 HTML 过程中遇到了 JavaScript 标签,则会停止 HTML 的解析,首先去解析 JavaScript 标签中的元素,解析 JavaScript 这一行为就交给我们刚才提到的 JS 引擎去做
  3. 当 DOM Tree 生成和将它和 CSS 解析生成的样式文件进行合并,也就是这里的 Attachment(附加)的过程;
  4. 接下来浏览器因为要考虑到不同设备之间元素如何定位,所以有一个 Layout 的过程;
  5. 结束后浏览器就会将 Render Tree 渲染到页面上进行展示

V8 引擎的原理

首先看一下官方对于 V8 引擎的定义:

  • V8 是用 C ++编写的 Google 开源高性能 JavaScript 和 WebAssembly 引擎,它用于 Chrome 和 Node.js 等。
  • 它实现 ECMAScript 和 WebAssembly,并在 Windows 7 或更高版本,macOS 10.12+和使用 x64,IA-32, ARM 或 MIPS 处理器的 Linux 系统上运行。
  • V8 可以独立运行,也可以嵌入到任何 C ++ 应用程序中

整体运行流程:

20220502142523

  1. 首先 V8 引擎拿到 JavaScript 代码会进入 parse 阶段,这个阶段会做两件事,就是词法分析和语法分析

    1. 词法分析: 将一条语句进行拆分,拆分至每个单词的细粒度类似 const 、name、=、' 、hello、',将字符流转换为词法单元流(token)
    2. 语法分析: 这一阶段会将词法分析得到的内容进行区分,哪些是标识符,变量等等,并生成 AST
  2. 完成第一阶段会生成一条条 tokens,所有的 tokens 组成我们的 AST(抽象语法树),可以访问这个网站进行查看 生成 ASTopen in new window

  3. 接下来就是将我们生成的 AST 交给 Ignition,Ignition 负责帮助我们将 AST 转换为字节码(byteCode)并且会对代码进行分析;

    1. 如果一个函数被多次调用,那么 Ignition 会将这个函数标记为热点函数,交由 TurboFan 编译为机器码,提高执行性能;
    2. 如果一个函数只执行了一次,那么 Ignition 会将其转换为 ByteCode;
    3. 当然还有一种情况是这个函数并没有进行调用,那么此时是不会对这个函数做任何操作的;
  4. TurboFan 是一个编译器,将字节码文件编译为机器码,如果一个函数已经被标记为热点函数,但是某一次执行时,传入的类型和之前的类型不同,比如之前传入数值,那么执行相加操作,但是如果传入字符串,这个时候就不是相加了,此时直接调用之前生成的机器码肯定是不行的,所以 TurboFan 会重新将其转换为字节码,进行调用(这也是解释了为什么编写 TS 会使程序性能会好一点);

那么为什么会有一个生成字节码的逻辑呢?直接生成机器码不是更好吗?
这是为了可以让代码在不同环境运行,比如说 windows 和 mac 上,更好的跨平台的特性

V8 引擎细节及解析图(官方)

V8 引擎解析图

知道了上述浏览器的整体流程之后,我们具体来看一下上述的 Parse 这个解析过程,这个过程内部具体实现就是如上图所示:

  1. 首先 Blink 将源码交给 V8 引擎,这里通过流的方式进行传输,并且约定了编码格式;
  2. Scanner 阶段就是之前说的词法分析和语法分析阶段,将代码转换为 tokens 再转换为 AST;
  3. 接下来 tokens 会被转换成 AST 树,经过 Parser 和 PreParser:
    1. Parser 就是直接将 tokens 转成 AST 树结构;
    2. PreParser 称之为预解析,为什么需要预解析呢?
      1. 这是因为并不是所有的 JavaScript 代码,在一开始时就会被执行。那么对所有的 JavaScript 代码进行解析,必然会影响网页的运行效率;
      2. 所以 V8 引擎就实现了 Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行;
      3. 比如我们在一个函数 outer 内部定义了另外一个函数 inner,那么 inner 函数就会进行预解析
  4. 生成 AST 树后,会被 Ignition 转成字节码(bytecode),之后的过程就是代码的执行过程。

JavaScript 简单变量的执行过程

var name = "beanBag";
var num1 = 10;
var num2 = 20;
var result = num1 + num2;
console.log(result);

以上代码如何执行:

  1. 首先 js 引擎在执行代码之前会在堆内存中创建一个全局对象:Global Object(GO);
    1. 该对象所有的作用域( scope )都可以访问;
    2. 里面还会包含 Date、String、setTimeOut 等等;
    3. 其中还有一个 window 对象属性指向自己(所以我们可以一直 window.window.window)

Global Object

  1. 其次,js 引擎内部还有一个执行上下文栈(Execution Context Stack,简称 ECS), 它是用于执行代码的调用栈
    1. 那么现在它要执行谁呢? 执行的是全局的代码块
      • 全局的代码块为了执行会构建一个 Global Execution Context(GEC);
      • GEC 会放到 ECS 中执行
    2. GEC 被放入到 ECS 中里面包含两部分内容:
      1. 在代码执行前,在 parser 转成 AST 的过程中,会将全局定义的变量、函数等加入到 GlobalObject 中,但是并不会赋值; 这个过程也称之为变量的作用域提升(hoisting);
      2. 在代码执行中,对变量赋值,或者执行其他的函数;

GEC被放入到ECS中

Last Updated:
Contributors: hqchqc