浏览器原理之二:JS、V8

438 阅读10分钟

浏览器原理第二篇:站在浏览器的角度,分析JavaScript执行机制;深入探讨来自Google的JavaScript引擎V8,分析其执行流程,细化讲解JavaScript中数据是如何存储和回收的,助你打造高性能且节约内存的Web应用。

07. 变量提升

08. 调用栈

编译执行

  1. 当 JS 执行全局代码时,会编译全局代码并创建全局执行上下文。
  2. 当调用一个函数时,函数体内部的代码会被编译,创建函数执行上下文。一般函数执行结束之后,创建的函数执行上下文会被销毁。
  3. eval 函数(执行时么?)的代码也会被编译,并创建执行上下文。

console.trace() 输出当前函数调用关系。

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);

09. 块级作用域

解决变量提升、变量覆盖、变量污染等 JS 设计缺陷。

作用域

指程序中定义变量的区域,该位置决定了变量的生命周期。通俗讲,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

全局作用域

在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。

函数作用域

在函数内部定义的变量或函数,定义的变量和函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

块级作用域

使用一对大括号包裹的一段代码。

JS 将块级作用域放到词法环境。(编译阶段,let const 声明的变量放到词法环境,而 var 声明的变量放到变量环境中的。)

10. 作用域链和闭包

词法作用域是指作用域是由代码中函数声明的位置来决定的,是静态作用域,与函数调用无关。

闭包

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。

11. this

this 与作用域链基本无关。每个执行上下文中都有一个 this。

  1. 函数被正常调用时,函数中的 this 指向 window,严格模式下 this 是 undefined;
  2. 函数作为对象的方法调用时,函数中的 this 就是该对象;
  3. 可以使用 call、apply、bind 方法显式设置函数执行上下文的 this;
  4. 嵌套函数中 this 不会继承外层函数的 this 值(解决方案:self 保存外层 this 或 箭头函数);
  5. 被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字指向 window;
  6. 构造函数的 this 指向新对象。

12. 栈空间和堆空间

语言类型

  • 静态语言:在使用之前需要确定变量数据类型的语言。C、C++、Java
  • 动态语言:在运行过程中需要检查数据类型的语言。JS、PHP、Python、Ruby、VB
  • 弱类型语言:支持隐式类型转换的语言。C、C++、JS、PHP、VB
  • 强类型语言:不支持隐式类型转换的语言。Java、Ruby、Python

原始类型的数据存放在栈中。(变量名:变量值)(a: 1)(user: 1003)

引用类型的数据存放在堆中。 (地址:值)(1003: { name: 'jkb', age: 2 } )

再谈闭包

编译过程中,遇到内部函数应用外部函数的变量,JS 引擎判断是一个闭包,会在堆空间创建一个内部对象 "closure(foo)" ,将用到的变量保存在"closure(foo)"中。

13. 垃圾回收

C、C++:使用手动回收策略,何时分配内存、何时销毁内存都是有代码控制的。

Javascript、Java、Python:使用垃圾回收器来释放产生的垃圾数据。

调用栈的数据回收

函数执行结束之后,JS 引擎通过向下移动 ESP(记录当前执行状态的指针)来销毁该函数保存在栈中的执行上下文。当新的函数执行上下文入栈时,直接覆盖原来的执行上下文。

堆中数据回收

函数执行完毕,栈内存释放。但是函数中对象使用的堆空间仍未释放,这就需要 JS 的垃圾回收器。

V8

两个堆的区域

  • 新生代:存放的是生存时间短的对象,容量 1~8M。
  • 老生代:存放生存时间久的对象。

两种垃圾回收器

  • 副垃圾回收器:主要负责新生代的垃圾回收。
  • 主垃圾回收器:主要负责老生代的垃圾回收。

共同执行流程

  1. 标记空间中活动对象和非活动对象。活动对象是还在使用的对象,非活动对象是可以进行垃圾回收的对象。
  2. 回收非活动对象占据的内存。即在标记完成后,统一清理内存中被标记为可回收的对象。
  3. 内存整理。频繁回收对象后,会存在大量不连续空间--内存碎片,最后一步需要整理这些内存碎片。(主垃圾回收器)

副垃圾回收器

新生代使用 scavenge 算法,将空间对半划分为对象区域、空闲区域。新加入的对象都存放在对象区域,对象区域写满时,开始一次垃圾清理操作。

  1. 给对象区域中的垃圾做标记,标记完成后进入垃圾清理阶段;
  2. 副垃圾回收器将存活的对象复制到空闲区域,并把它们有序的排列起来(无内存碎片);
  3. 复制完成后,对象区域和空闲区域进行角色反转;
  4. 如果经过两次垃圾回收依然还存活的对象,会被移动到老生区。

主垃圾回收器

老生区中对象的特点:占用空间大、存活时间长。

标记-清除算法(Mark - Sweep)

  1. 垃圾标记过程:从一组根元素开始,遍历,能到达的称为活动对象,没有达到的判为垃圾数据。
  2. 垃圾清除过程:直接对可回收对象进行清理。

标记-整理算法(Mark - Compact)

标记过程一样,然后让所有存活对象向一端移动,最后直接清理端边界外的内存。

全停顿

JS 和垃圾回收都是运行在主线程上的。一旦执行垃圾回收,JS 脚本会暂停,待垃圾回收完毕后再恢复脚本执行,这种行为被叫做全停顿。

为降低老圣地啊的垃圾回收造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JS 应用逻辑交替进行,直到标记完成,这个算法叫增量标记算法。

14. 编译器和解释器

编译器

编译型语言,在程序执行之前,编译器将源代码编译成而二进制文件,每次运行程序时,直接运行二进制文件,无需再编译。如 C、C++、GO

工作流程:

  1. 源代码 词法分析、语法分析 >> AST;
  2. AST 词义分析 >> 中间代码;
  3. 中间代码 代码优化 >> 二进制文件,直接执行。

解释器

每次运行时需要通过解释器对程序进行动态解释和执行。如 Python、JS。

工作流程:

  1. 源代码 词法分析、语法分析 >> AST;
  2. AST 词义分析 >> 字节码,解释执行。

AST

JS 执行代码第一步,生成 AST 和执行上下文。

  1. 词法分析,将一行代码拆分成一个个 token(如 keyword、identifier、assignment 赋值、literal 字符串...);
  2. 语法分析,将生成的 token 数据,根据语法规则转为 AST,如果源码符合语法规则就会顺利完成,如果存在语法错误就终止并抛出"语法错误"。

字节码

JS 执行代码第二步,生成字节码。

  1. 解释器 Ignition 根据 AST 生成字节码。
  2. 解释器 Ignition 逐条解释执行字节码。发现热点代码(被重复执行多次),后台的编译器 TurboFan 会将热点代码编译为高效的机器码并保存备用。

字节码介于 AST 和机器码之间,需要解释器将其转换为机器码才能执行。内存占用比机器码少。

字节码配合解释器和编译器的技术被称为即时编译 (JIT)。

JS 性能优化

  1. 提升单次脚本执行速度,避免 JS 长任务霸占主线程。
  2. 避免大的内联脚本。
  3. 较少 JS 文件的容量。

附:

babel 工作原理

先将 ES6 源码转换成 AST,然后再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JS 源码。

WebAssembly

assembly程序集。

一种技术方案,使用非JS编程语言编写代码并能在浏览器运行。

使用C、C++、Rust编写代码,使用LLVM(编译器工具链)编译成.wasm文件(可在JS中加载使用)。

JS解释器和编译器

将JS代码翻译成机器语言。

  1. 解释器:翻译过程是一行一行进行的,执行到哪行,翻译哪行。
  2. 编译器:执行前翻译。
监视器

JS引擎的一部分。在JS运行时监控代码,记录代码片段运行的次数以及使用的数据类型。运行几次的被标记为"warm",交由基础编译器(小幅提速);运行很多次的被标记为"hot",交给优化编译器(大幅提速)。

JS vs WebAssembly

JS:download >>> parse >>> compile + optimize >>> re-optimize >>> execute >>> garbage collection

WebAssembly:download >>> parse >>> compile + optimize >>> execute

  1. download:相同功能,WebAssembly文件比JS文件小。
  2. parse:JS源码先被解析为抽象语法树,然后需要的代码被转换为字节码,不需要的被创建存根。WebAssembly是字节码,只需要被解析并确定有没有错误。
  3. compile + optimize:JS动态类型语言,执行期间编译,相同代码多次执行可能因数据类型不同被重新编译。而WebAssembly字节码不需要在运行时判定数据类型。
  4. re-optimize:JS代码基于运行时的假设不正确是,会重新优化。WebAssembly类型明确不需要重新优化。
  5. execute:WebAssembly为编译器设计,提供更适合机器的指令,运行速度更快。
  6. garbage collection:JS引擎使用垃圾回收器自动进行垃圾回收(无法控制时机)。WebAssembly不支持垃圾回收,内存需要手动管理

系列文章

浏览器原理之一:大体看看

浏览器原理之三:页面

浏览器原理之四:网络安全