02JS运行原理

137 阅读8分钟

javaScript是什么?

javaScript是一门高级的编程语言。而我们从编程语言发展历史来说,可以划分为三个阶段:

  1. 机器语言:1000100111011000,一些机器指令;
  2. 汇编语言:mov ax,bx,一些汇编指令;
  3. 高级语言:C、C++、Java、JavaScript、Python;

高级语言:

  1. 编译型语言:将代码编译成可执行文件,直接放在电脑里面进行执行。
    • C、C++
    • Java目前还存在争议。
  2. 解析型语言:一边读代码一边进行解释。
    • JavaScript、Python

转化

对于计算机本身来说,它并不认识这些高级语言,所以我们的代码最终还是需要被转化为机器指令。

浏览器工作原理

  1. 当我们在浏览器输入域名的时候,会通过DNS域名解析,解析成为一个ip地址,这个ip地址就是服务器的地址,然后当我们敲回车的时候,服务器就会给我们返回一个index.html
  2. 在浏览器对html解析的过程中,比如遇到了link引入的CSS文件,那么就会将CSS文件下载下来,然后继续进行解析,如果遇到js文件,那么同样的道理,也会把js文件下载下来。

那么这些代码是如何没浏览器进行解析的呢?那么就要提出另外一个概念,叫做浏览器内核。

浏览器内核

我们可能会听过这样一句话:不同的浏览器是由不同的内核组成的。

常见的浏览器内核

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

我们经常说的浏览器内核指的是浏览器的排版引擎:

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

认识JS引擎

为什么需要JS引擎

  1. 我们前面说过,高级的编程语言都是需要转成最终的机器指令来执行的;
  2. 事实上我们编写的JavaScript无论你交给浏览器或者Node执行,最后都是需要被CPU执行的;
  3. 但是CPU只认识自己的指令集,实际上是机器语言,才能被CPU所执行;
  4. 所以我们需要JavaScript引擎帮助我们将JavaScript代码翻译成CPU指令来执行;

常见的JS引擎

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

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

这里我们以WebKit为例,WebKit事实上由两部分组成的:

image-20220817200112336

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

在小程序中编写的JavaScript代码就是被JSCore执行的;

V8引擎的原理

定义

官方:V8是用C ++编写的Google开源高性能JavaScript和WebAssembly(未来js的替代产品)引擎,它用于Chrome和Node.js等。

它实现ECMAScript和WebAssembly,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32, ARM或MIPS处理器的Linux系统上运行。

V8可以独立运行,也可以嵌入到任何C ++应用程序中。

V8引擎的架构

V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对JavaScript执行的:

  1. Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;
    • 如果函数没有被调用,那么是不会被转换成AST的;
  2. Ignition是一个解释器,会将AST转换成ByteCode(字节码)
    1. 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
    2. 如果函数只调用一次,Ignition会执行解释执行ByteCode;
  3. TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;
    • 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
    • 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是 number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;

V8的细节

image-20220817202717486

那么我们的JavaScript源码是如何被解析(Parse)过程的呢?

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

初始化全局对象

执行过程

var name = "cs";
function foo() {
  var name = "foo";
  console.log(name);
}

var n1 = 20;
var n2 = 30;
var res = n1 + n2;
console.log(res);

foo()

假如我们有上面一段代码,它在JavaScript中是如何被执行的呢?

image-20220817215927321

js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)

  1. 该对象所有的作用域(scope)都可以访问,就相当于是window对象;
  2. 里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
  3. 其中还有一个window属性指向自己;

执行上下文

js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈。

那么现在它要执行谁呢?执行的是全局的代码块:

  1. 全局的代码块为了执行会构建一个 Global Execution Context(GEC);
  2. GEC会 被放入到ECS中 执行;

GEC被放入到ECS中里面包含两部分内容:

  1. 在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值;这个过程也称之为变量的作用域提升(hoisting)。
  2. 在代码执行中,对变量赋值,或者执行其他的函数;

认识VO对象:

  1. 每一个执行上下文会关联一个VO(Variable Object,变量对象),变量和函数声明会被添加到这个VO对象中。
  2. 当全局代码被执行的时候,VO就是GO(关联一个VO)对象了

全局代码执行过程

image-20220817221823516

image-20220817221844030

函数的执行

在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC),并且压入到EC Stack中。

因为每个执行上下文都会关联一个VO,那么函数执行上下文关联的VO是什么呢?

  1. 当进入一个函数执行上下文时(函数没有执行的时候,不会创建AO),会创建一个AO对象(Activation Object);
  2. 这个AO对象会使用arguments作为初始化,并且初始值是传入的参数;
  3. 这个AO对象会作为执行上下文的VO来存放变量的初始化;
  4. 当执行完成之后,会销毁(弹出栈)掉;
  5. 当重复调用函数的时候,会创建一个新的AO(和原来那个毫无关系),原来那个大概率会被销毁掉(不排除闭包)

函数的执行过程

image-20220817224549746

image-20220817224632381

作用域和作用域链

当进入到一个执行上下文时,执行上下文也会关联一个作用域链(Scope Chain)。

  • 作用域链是一个对象列表,用于变量标识符的求值;
  • 当进入一个执行上下文时,这个作用域链被创建,并且根据代码类型,添加一系列的对象;
  • 作用域链是在全局代码被解析时候,就已经创建好了。和调用的位置没有关系。
var message = "global message";

function foo(){
	console.log(message);//global message
}

function bar(){
	var message = "bar message";
	foo();
}
bar()

当查找变量的时候,优先在自己的VO里面进行查找。