本文章知识来源b站upobjtube的卢克儿
- 【干货】8分钟带你了解JS运行原理!写了那么多代码,还不知道JS是如何被运行的?
- 【干货】8分钟带你了解V8引擎是如何运行JS!都2020年了还不知道什么是V8?
- 【干货】6分钟带你掌握JS调用栈 | JS运行原理系列3
1. JS是如何被编译的
JavaScript初认识
JavaScript是由Brenddan Eich在1995年创建的,JavaScript在初期设计时基本就是很多语言的大杂烩
- 借鉴了C语言的基本语法
- 借鉴了Java语言的数据类型和内存管理
- 借鉴了Schema语言将函数提升到”第一等公民“的地位
- 借鉴了Self语言使用了基于原型prototype的继承机制
JavaScript实际上是函数式编程+面向对象编程的混合产物,在JavaScript中我们定义变量是完全不用关系他的类型的,但是像C++这样的就必须规定变量的类型,所以JavaScript被称为动态类型语言。
- 在JavaScript里,我们可以在声明赋值之后可以随意添加、删除里面的属性。
- 因为源代码里给编译器提供的信息太少了,使得编译器没有办法在运行前知道变量的类型
- 只有在运行期间才能确定各个变量的类型,这就导致JS无法在运行前编译出更加迅速的低级语言代码->机器代码
- 相反,使用c++这些语言时,因为提供了足够的类型信息来帮助编译器编译出机器代码
JIT
尽管JavaScript是一门动态语言,但是现在的JavaScript运行起来依然是很迅速。原因:现在的JS引擎都使用了一项技术Just-In-Time Compilation
(运行时编译),简称JIT,一种提高程序运行效率的方法
- JIT就是在运行阶段生成机器代码,而不是提前生成
- JIT把运行代码和生成机器代码结合在一起,在运行阶段收集类型信息,然后根据这些信息编译生成机器码
- 之后在运行这些代码就直接使用生产好的机器代码
AOT
AOT是另一种方法,在运行前提前生成好机器代码,比如像C++这样的语言
JS引擎
JS作为高级程序语言,在被计算机CPU执行前,需要通过JS引擎将JS转换成低级的机器语言并执行
JS引擎列举:
-
谷歌Chrome使用的V8引擎
-
webkit使用的JavaScriptCore
-
Mozilla的SpiderMonkey
JS引擎编译JS的流程
- 首先将JS源码通过解析器,解析成抽象语法树AST
- 接着在通过解释器将AST编译成字节码
bytecode
- 字节码在通过编译器生成机器代码(汇编代码),编译器会根据不同的平台编译出相应的机器代码
2. JS引擎
1. 介绍
用到V8引擎的程序
- Chrome浏览器的JS引擎是V8
- Nodejs的运行时环境是V8
- electron的底层引擎是V8 -> 跨平台桌面应用开发工具
拓展
-
blink是渲染引擎,V8是JS引擎
-
访问Dom的接口是由Blink提供的
功能
接收JavaScript代码,编译代码后执行C++程序,编译后的代码可以在多种操作系统多种处理器上运行
- 编译和执行JS代码
- 处理调用栈
- 内存分配
- 垃圾回收
2. V8对于JS的编译和执行
大部分JS引擎在编译和执行JS代码时,都会用到三个重要的组件:解析器、解释器、编译器
解析器parser
- 负责将js源代码解析成抽象语法树
AST
解释器interpreter
- 负责将AST解释成字节码
bytecode
- 解释器也有直接解释执行
bytecode
的能力(跳过AST
)
编译器compiler
- 负责编译出运行更加高效的机器代码
3. 早期的V8引擎
在V8早期5.9版本之前,V8引擎没有解释器,有两个编译器
编译流程
JS代码 --(解析器parser)–> AST抽象语法树 --(Full-codegen编译器)–> 机器代码
- Full-codegen编译器:也称为基准编译器,他生成的是一个基准的未被优化的机器代码
- 这样做的好处就是,当第一次执行js代码时就是直接使用了高效的机器代码,因为没有中间字节码的产生
- 当按照上面的流程代码运行一段时间后,V8引擎中的分析线程收集了足够的数据来帮助另一个编译器
Crankshaft
来做代码优化 - 需要优化的源码重新解析生成AST
Crankshaft
使用生成好的AST再生成优化后的机器代码,从而提升运行的效率- 完整步骤
评价
- 好处:
- 减少了抽象语法树
AST
到字节码的转换时间,提高外部浏览器中JS的执行性能
- 减少了抽象语法树
- 弊端:
- 生成的机器码会占用大量的内存。对于早期低内存的安卓设备来说是不能承受的
- 缺少中间层机器码,很多性能优化策略无法实施,导致V8引擎性能提升缓慢
- 用到的编译器无法很好的支持和优化JS的新语法特性
4. 改进后的V8引擎
编译流程
JS代码 --(parser解析器)–> AST --(lgniton基准解释器)–> bytecode字节码 ----> AST被清除 ----> bytecode直接被解释器执行 --(lgniton收集代码优化信息)–> TruboFan编译器
- 新增了
lgniton
基准解释器 - 在生成bytecode字节码的同时,AST被清除掉,释放内存空间
- 生成的bytecode直接被解释器执行
- 同时生成的bytecode将作为基准执行模型,字节码更加简洁
- 生成的bytecode大小相当于等效的基准机器代码的25%到50%左右
- 在代码的不断的运行过程中,解释器收集到了很多,可以用来优化代码的信息
- 比如变量的类型、哪些函数执行的频率较高
- 这些信息被发送给优化编译器
TruboFan
TruboFan
编译器会根据这些信息和字节码,来编译出经过优化的机器代码- 优化过后得到的机器代码可能会被逆向还原成字节码,这个过程叫做
deoptimization
- 这是因为JavaScript是一个动态语言,会导致一个lgnition收集到的信息是错误的
- 比如有这样的一个sum函数,在函数声明时,JS引擎并不知道参数x,y是什么类型,但在后面的多次调用中,传入的x,y都是整型,sum函数被识别为热点函数
- 解释器将收集到的类型信息和该函数对应的字节码发送给编译器
- 于是编译器生成的优化后的机器代码中就假定了sum函数的参数x,y都是整型
- 之后遇到该函数的调用,就直接使用运行更快的机器代码
- 如果此时你调用sum函数传入了字符串,机器代码不知道如何处理字符串的参数
- 于是就需要进行deoptimization,也就是回退字节码,由解释器来解释执行
V8引擎的优化策略
- 函数只声明未被调用,不会被解析生成AST
- 函数只被调用一次,则Ignition生成字节码后,bytecode直接被解释执行,Turbofan不会进行优化编译
- 因为它需要lgnition收集函数,执行时的类型信息,这就要求函数至少执行大于一次,TurboFan才能够进行优化
- 函数被调用多次,可能会被标记为热点函数,当lgnition解释器收集的类型信息确定后,这时TurboFan则会将bytecode编译为优化后的机器代码,以提高代码的执行性能。之后执行这个函数时,就直接运行优化后的机器代码
- 由于不需要一开始直接编译成机器码,而是生成了中间层的字节码,字节码的生成速度是远远大于机器码的,所以网页初始化解析执行JS的时间缩短了,缩短了网页加载的时间
- 在生成的优化机器代码时,不需要从源码重新编译,而使用字节码。并且当需要deoptimization时,只需要回归到中间层的字节码解释执行就可以了
3. JS调用栈
1. 什么是调用栈
-
调用栈是JS引擎追踪函数执行流程的一种机制,当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体又调用了哪个函数。
-
它采用了先进后出的机制来管理函数的执行
-
举个栗子:
function sum(a, b){ return a + b; } function average(a, b){ const aver = sum(a,b) / 2; return aver / 2; } const num = average(3, 5); console.log(num)
-
函数的声明是不会放入栈中的,调用栈,顾名思义一定是被调用的函数才会入栈
-
所以直到运行到
const num
的时候average()
函数才被调用 -
把average的执行添加到调用栈,然后执行average函数体中的所有代码
-
当进入
average
函数体中发现调用了sum()
函数,接着将sum()
函数入栈 -
执行
sum()
函数体中的代码,直到sum()
函数执行完毕,然后继续执行sum函数后面的代码 -
接着sum函数出栈,从栈顶移除。等到
average
函数执行完毕,等到执行average
函数后面的代码,average
函数出栈。 -
最后执行
console.log()
,console.log()
入栈,执行完毕console.log()
出栈 -
调用栈被清空
-
查看某一行代码所处的调用栈环境
介绍两种方法
- 给某一行代码打断点
- 抛出异常
2. 堆栈溢出
触发堆栈溢出最常见的就是递归
举一个栗子:
function sum(){
sum()
}
sum()
-
如上面的函数,sum函数每被调用一次就执行sum函数,sum函数被推入调用栈,sum函数里又调用sum函数,由于没有停止条件,sum函数就会不断的被推进调用栈
-
在很短的一个时间内就会超出调用栈的一个堆栈限制,这种行为被称为
溢出overflowing
-
在控制台会展示这样一个错误,告诉你当前调用栈已经超出了最大使用范围,然后网页就会卡死
栈溢出带来的问题
当前浏览器标签页里的JS执行出现了溢出,会影响其他标签页,原因如下:
- JavaScript的执行环境是一个单线程,这也就意味着JS环境只有一个调用栈
- 如果调用栈的某个函数执行需要消耗大量时间的话,就会导致调用栈被阻塞,无法入栈和出栈
- JS页面的绘制和布局都是在一个主线程里,如果JS执行迟迟不归还主线程的话,就会影响页面的渲染,就可能会导致页面出现卡顿的现象
优化这个问题的解决方案
- 使用事件循环和异步回调