深入JavaScript高级语法-学习笔记-01-浏览器原理-V8引擎-JS执行原理

203 阅读6分钟

浏览器的工作原理

JavaScript的代码在浏览器中是如何被执行的? 当我们访问一个网址,这个网址首先会被DNS解析为IP地址,然后通过这个ip地址去访问访问服务器,随后,服务器给我们返回一个index.html文件,当html文件解析过程中碰到了link标签才去下载css文件,同理碰到script标签才去下载js文件。

浏览器渲染过程

浏览器渲染首先加载html文件,经过HTML Parser进行解析,此时如果解析的过程中遇到js标签,就会停止解析HTML,而去加载和执行JavaScript代码,两者结合后生成DOM Tree。同时,CSS文件经过CSS Parser进行解析生成Style Rules(样式规则),DOM Tree与Style Rules进行结合生成Render Tree,这时根据浏览器不同的Layout布局对Render Tree进行调整,最后经过Painting绘制在浏览器上显示出来。

V8引擎的原理

image.png JavaScript源代码在V8引擎中执行的过程

1、JavaScript源代码首先经过Parse模块解析,在这个过程中首先会进行词法分析,词法分析就好比把JavaScript的代码一块块分开,对每个词进行分析,分析后会得到一个tokens[{},{},...],这个tokens是一个数组,里面是多个对象,包括的内容是类似于{type:"", value:""}这样的对象,合并起来组成的数组。然后再根据这个tokens数组中的对象对代码进行语法分析,最后形成AST抽象语法树。

2、形成AST抽象语法树后,AST经过V8引擎中Ignition库形成对应的字节码,不直接生成机器码是因为V8引擎可在不同的系统环境中运行,而不同的系统架构所对应的机器码不同,ignition库无法判断当前处于哪种架构中,因而生成的是可以跨平台的字节码(bytecode),字节码再根据不同的平台转化为对应的汇编指令,再转为对应的机器码。

3、在解析过程中,如果发现某一段代码,比如说一个函数,反复的执行,那这样是有些浪费性能的,每一次都需要走字节码转汇编再转机器码这一步,所以在V8引擎中还有一个库叫做TurboFan,这个库可以给这个反复执行的函数打上一个热点(hot)标记,这样它执行的时候就可以直接转化为优化过后的机器码,无需再转为字节码。

4、假如说这个反复执行已被TurboFan优化过的函数突然运行的方式变了,比如说原先传的参数都是数字,突然传的参数都是字符串,那函数内容的运算方式就不同了,这时候被优化过的机器码会重新转化为字节码,再执行上面的过程。

V8引擎的解析图(官方)

在官方提供的V8引擎的解析图中与上述所说过程类似。

blink内核拿到JavaScript源码后以流的形式传递给Scanner,由Scanner进行词法分析,生成tokens交给Parser模块进行语法分析生成AST树后交给Ignation模块生成字节码,过程与前一段描述过程相同。

在官方的解析图提到PreParser这个模块,它的作用是预解析,比如说我这有一段代码

function outer {
  function inner(){
    var inner = 'inner'
    console.log(inner)
  }
}

outer()

在上面这段代码中inner这个函数从始至终没有被调用过,因此完全没有必要进行完全解析,所以它就会进行预解析,以此来节省性能。

JavaScript执行的过程(ES5)

// 假如说我们这里有一些简单的代码
var name = "dyy"

var num1 = 20
var num2 = 30
var result = num1 + num2

/**
*  1.代码被解析, v8引擎内部给我们创建了一个对象GlobalObject(GO)
*  2.运行代码
*     2.1. v8为了执行代码,v8引擎内部会有执行上下文栈(Execution Context Stack, ECS/ECStack)(函数调用栈)
*     2.2. 因为我们执行的是全局代码,为了全局代码能够正常的执行,需要创建全局执行上下文(Global Execution Context)(全局代码需要被执行时才会创建)
*/
// 下面写一段伪代码
var globalObject = {
  String: "类",
  Date: "类",
  setTimeout:"函数",
  window: GlobalObject,
  name: undefined,
  num1: undefined,
  num2: undefined,
  result: undefined
}

首先,通过前面我们知道,在V8引擎的原理中,第一步是JavaScript经过词法分析和语法分析转化为AST树,这个过程就是JavaScript解析的过程,在这个过程中V8会给我们生成一个GlobalObject对象(即GO),我们在平时书写代码的时候有些类啊函数之类的比如说String/Date/Number/setTimeout/setInterval/window:this,像这些可以直接使用的内容都是在解析的过程中V8引擎给我们添加进入GO对象中的。

我们在全局定义的属性也会在解析的阶段加入GO中,不过由于此时代码尚未执行,所以在GO中有这些属性声明,但值为undefined。这个过程就是变量的作用域提升。

随后便到了JavaScript代码的执行过程。在这个过程中,为了使全局代码能够正常的执行,需要创建全局执行上下文(GEC),然后将全局执行上下文放入执行上下文栈中执行,全局执行上下文中维护了一个对象叫做变量对象Variable Object(VO),对于全局执行上下文中,VO指向的就是GO。代码开始执行,便会先从VO中寻找对应的变量,在全局执行上下文中最终就会找到GO中,根据代码依次修改值,若是在变量执行修改前打印值,便能发现此时变量尚未被赋值,打印出为undefined。

var name = "why"

foo(321) // 321
function foo(num) {
  console.log(m) // undefined
  var m = 10
  var n = 20
  console.log(num)
}

但是函数有所不同,当我们将在函数定义之前调用函数,我们会发现函数依然可以正常调用,这是为什么呢?

在JavaScript解析阶段时,函数是比较特殊的,当代码解析时遇到函数,它在GlobalObject中存储的并不是如其他变量那样的undefined,而是一个如0xa00这样的地址值,这个地址指向的是JS引擎在遇到函数时为函数开辟的一个存储空间,在这个存储空间中存储了函数的父级作用域parent scope以及函数的执行体(代码块)。当函数执行时,将会调用存储函数空间中存储的内容并创建一个函数执行上下文(Functional Execution Context,FEC),并放入ECS调用栈中,在FEC中同样也有一个VO对象,不过它指向的是AO(Activation Object)对象,此对象在函数执行时创建,内部即是函数内部的变量,当函数执行完毕后,FEC将会出栈并销毁,AO对象也会销毁,然后等到下一次调用函数时,将会再次重复这个流程。