"浅谈"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的 内存管理 以及 闭包 的知识吧

本文深入探讨了JavaScript在浏览器中的执行过程,从输入URL开始,讲解了HTML解析、CSS下载以及JavaScript的执行。重点介绍了V8引擎的工作机制,包括Pars模块将JavaScript代码转换成AST、Ignition解释器生成ByteCode以及TurboFan编译器优化性能。还讨论了预解析、函数执行上下文、作用域链和变量提升的概念。最后,简要提及了JavaScript的内存管理和闭包知识。
896

被折叠的 条评论
为什么被折叠?



