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 实际上是由两部分组成的:
- WebCore:负责解析、渲染、布局等工作;
- JavaScriptCore: 解析和执行 JavaScript 代码;
实际上,这和我们小程序的架构非常类似:
- 小程序中我们的 JavaScript 代码就是 JavaScriptCore 负责执行的
另外一个非常强大的引擎就是我们经常听到的 V8 引擎。
4. 浏览器的渲染过程
上图是一个浏览器解析我们文件的具体过程:
- 首先浏览器拿到我们的文件,从上往下执行:
- 如果是样式文件,交给 CSS Parser 处理解析
- 如果是 HTML 标签,交由 HTML Parser 处理解析
- 由于我们的 JavaScript 文件可以操纵 DOM 元素,所以势必会影响到 HTML 解析生成 DOM Tree 的过程,所以如果解析 HTML 过程中遇到了 JavaScript 标签,则会停止 HTML 的解析,首先去解析 JavaScript 标签中的元素,解析 JavaScript 这一行为就交给我们刚才提到的 JS 引擎去做
- 当 DOM Tree 生成和将它和 CSS 解析生成的样式文件进行合并,也就是这里的 Attachment(附加)的过程;
- 接下来浏览器因为要考虑到不同设备之间元素如何定位,所以有一个 Layout 的过程;
- 结束后浏览器就会将 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 ++ 应用程序中
整体运行流程:
首先 V8 引擎拿到 JavaScript 代码会进入 parse 阶段,这个阶段会做两件事,就是词法分析和语法分析
- 词法分析: 将一条语句进行拆分,拆分至每个单词的细粒度类似 const 、name、=、' 、hello、',将字符流转换为词法单元流(token)
- 语法分析: 这一阶段会将词法分析得到的内容进行区分,哪些是标识符,变量等等,并生成 AST
完成第一阶段会生成一条条 tokens,所有的 tokens 组成我们的 AST(抽象语法树),可以访问这个网站进行查看 生成 AST;
接下来就是将我们生成的 AST 交给 Ignition,Ignition 负责帮助我们将 AST 转换为字节码(byteCode)并且会对代码进行分析;
- 如果一个函数被多次调用,那么 Ignition 会将这个函数标记为热点函数,交由 TurboFan 编译为机器码,提高执行性能;
- 如果一个函数只执行了一次,那么 Ignition 会将其转换为 ByteCode;
- 当然还有一种情况是这个函数并没有进行调用,那么此时是不会对这个函数做任何操作的;
TurboFan 是一个编译器,将字节码文件编译为机器码,如果一个函数已经被标记为热点函数,但是某一次执行时,传入的类型和之前的类型不同,比如之前传入数值,那么执行相加操作,但是如果传入字符串,这个时候就不是相加了,此时直接调用之前生成的机器码肯定是不行的,所以 TurboFan 会重新将其转换为字节码,进行调用(这也是解释了为什么编写 TS 会使程序性能会好一点);
那么为什么会有一个生成字节码的逻辑呢?直接生成机器码不是更好吗?
这是为了可以让代码在不同环境运行,比如说 windows 和 mac 上,更好的跨平台的特性
V8 引擎细节及解析图(官方)
知道了上述浏览器的整体流程之后,我们具体来看一下上述的 Parse 这个解析过程,这个过程内部具体实现就是如上图所示:
- 首先 Blink 将源码交给 V8 引擎,这里通过流的方式进行传输,并且约定了编码格式;
- Scanner 阶段就是之前说的词法分析和语法分析阶段,将代码转换为 tokens 再转换为 AST;
- 接下来 tokens 会被转换成 AST 树,经过 Parser 和 PreParser:
- Parser 就是直接将 tokens 转成 AST 树结构;
- PreParser 称之为预解析,为什么需要预解析呢?
- 这是因为并不是所有的 JavaScript 代码,在一开始时就会被执行。那么对所有的 JavaScript 代码进行解析,必然会影响网页的运行效率;
- 所以 V8 引擎就实现了 Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行;
- 比如我们在一个函数 outer 内部定义了另外一个函数 inner,那么 inner 函数就会进行预解析
- 生成 AST 树后,会被 Ignition 转成字节码(bytecode),之后的过程就是代码的执行过程。
JavaScript 简单变量的执行过程
var name = "beanBag";
var num1 = 10;
var num2 = 20;
var result = num1 + num2;
console.log(result);
以上代码如何执行:
- 首先 js 引擎在执行代码之前会在堆内存中创建一个全局对象:Global Object(GO);
- 该对象所有的作用域( scope )都可以访问;
- 里面还会包含 Date、String、setTimeOut 等等;
- 其中还有一个 window 对象属性指向自己(所以我们可以一直 window.window.window)
- 其次,js 引擎内部还有一个执行上下文栈(Execution Context Stack,简称 ECS), 它是用于执行代码的调用栈
- 那么现在它要执行谁呢? 执行的是全局的代码块
- 全局的代码块为了执行会构建一个 Global Execution Context(GEC);
- GEC 会放到 ECS 中执行
- GEC 被放入到 ECS 中里面包含两部分内容:
- 在代码执行前,在 parser 转成 AST 的过程中,会将全局定义的变量、函数等加入到 GlobalObject 中,但是并不会赋值; 这个过程也称之为变量的作用域提升(hoisting);
- 在代码执行中,对变量赋值,或者执行其他的函数;
- 那么现在它要执行谁呢? 执行的是全局的代码块