"浅谈"JavaScript执行过程
前言:浅谈系列属于个人总结,如果有错误请各位大佬指正
浏览器的工作原理
JavaScript代码,在浏览器是如何被执行的?
当我们在输入服务器地址的时候,例如www.baidu.com,首先会解析 index.html
- 遇到link标签css文件,就会去服务器下载css文件
- 遇到script标签,就会去服务器下载JavaScript文件
浏览器渲染过程
当我们解析HTML的时候,遇到了JavaScript会交给谁去处理呢?
认识JavaScript引擎
-
为什么需要JavaScript引擎呢?
- 高级的编程语言都是需要转成 最终的机器指令来执行 的
- 我们编写的JavaScript无论是交给浏览器或者Node执行,最后都需要被CPU执行
- 需要JavaScript引擎帮助我们将JavaScript代码翻译成CPU指令来执行
V8引擎
-
Pares模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不认识JavaScript代码
- 如果函数没有被调用,那么是不会被转化成AST的
const name = "Agility" // 这里Pares是如何进行分析最终转化成AST呢?
astexplorer.net/ 这个网站可以看的转化成AST
-
Ignition是一个解释器,会将AST转化成ByteCode(字节码)
- 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类才能进行真实的运算)
- 如果函数只调用一次,Ignition会执行解释行ByteCode
-
TurboFan是一个编译器,可以将字节码编译为CPU可以直接进行的机器码
- 如果一个函数被多次调用,那么就会被比标记为 热点函数 ,那么就会经过 TurboFan转换成优化的机器码,提高代码的执行性能
- 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是 number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码
// 例如这是一个热函数 function sum(num1 , num2) { num1 + num2 } sum(10,20) // 正常调用直接从TurboFan运行结果 sum("a" , "b") // string类型,需要优化机器码再运行结果
V8执行的细节
那么我们的JavaScript源码是如何被解析(Parse过程)的呢?
-
内核(Blink)将源码交给V8引擎,Stream获取到源码并且进行编码转换;
-
Scanner(扫描器)会进行词法分析(lexical analysis),词法分析会将代码转换成tokens;
-
接下来tokens会被转换成AST树,经过Parser和PreParser:
-
Parser就是直接将tokens转成AST树架构;
-
PreParser称之为预解析,为什么需要预解析呢?
- 这是因为并不是所有的JavaScript代码,在一开始时就会被执行。那么对所有的JavaScript代码进行解析,必然会 影响网页的运行效率;
- 所以V8引擎就实现了Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行;
- 比如我们在一个函数outer内部定义了另外一个函数inner,那么inner函数就会进行预解析;
-
-
生成AST树后,会被Ignition转成字节码(bytecode),之后的过程就是代码的执行过程(后续会详细分析)
// 预解析 function outer() { function inner() { } }
总结
简单来说,我们的浏览器是可以解析HTML、CSS,但是碰到JavaScript就无法进行解析了,所以v8引擎诞生了,而v8引擎的解析步骤,可以分解为
解析代码:arrow_right:
生成AST树,其中做了优化步骤PreParser(预解析):arrow_right:
转成字节码,再次做优化TurboFan(热函数):arrow_right:
运行结果
JavaScript的执行过程
// 例如 var test = 'Agility' var num1 = 20 var num2 = 30 var result = num1 + num2 /* 1. 代码被解析v8引擎内部会帮助我们创建一个对象Global Object(GO) 2. 运行代码 2.1 v8引擎为了执行代码,v8引擎内部会有一个执行上下文栈(函数调用栈) Execution Context Stack,简称ECS 2.2 因为我们执行的是全局代码,为了全局代码能够正常的执行,需要创建全局执行上下文 */ var globalObject = { String: "类" Data: "类" window: globalObject, ... // 这个时候代码解析,但是没有运行 test: undefined num1: undefined num2: undefined result: undefined }
总结
所以就可以解释变量提升的原理,这里不针对函数是如何执行的
-
初始化全局对象Global Object(GO)
- 里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
- 其中还有一个 window属性指向自己 ;
-
执行上下文栈/函数调用栈(Execution Context Stack,简称ECS)
- 它是用于执行代码的调用栈
-
创建全局执行上下文Global Execution Context(GEC)
- 执行的是全局代码,为了全局代码能够正常的执行,需要创建全局执行上下文
- 在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中, 但是并不会赋值( 变量的作用域提升 )
- 在代码执行中,对变量赋值,或者执行其他的函数
JavaScript函数的执行过程
foo() // foo function foo() { console.log('foo') }
这时候我们发现函数可以正常执行,下面就让我们看看V8引擎在执行函数的过程吧
var test = Agility foo() function foo() { console.log(num) var num = 123 console.log('foo') }
-
在执行的过程中执行到一个函数時,会根据函数体创建一个函数执行上下文(Functional Execution Context, 简称FEC),并且压入到ECStack中。----- 放入
- FEC中包含三部分的内容
- 第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO): ü AO中包含形参、arguments、函数定义和指向函数对象、定义的变量;
- 第二部分:作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找;
- 第三部分:this绑定的值
-
FEC开始执行 ----- 执行
作用域链
FEC中还包含了作用域链, 父级VO在开始就被确定了 ,看看下面的代码
:arrow_right:当前作用域中存在
var num = 123 foo() function foo() { var num = 456 console.log(num) // 456 }
当前作用域中不存在
var num = 123 foo() function foo() { console.log(num) // 123 }
可以清楚的看出是如何沿着作用域链寻找
嵌套函数
我们再来看看嵌套函数的执行过程吧
var num = 123 foo(456) function foo(value) { console.log(m) var m = 10 var n = 20 function bar() { console.log(num) } bar() }
JavaScript执行过程就聊完了~:grinning:接下来就来看看JavaScript的 内存管理 以及 闭包 的知识吧