V8 是如何执行 JavaScript 代码的

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: V8 是如何执行 JavaScript 代码的

1. 前言

  1. 可能有很多道友好奇 chrome如何执行js代码的,而且经常听说chrome的V8引起运行 js 非常快.
  2. 今天试着写篇类似读书笔记吧,完全参考这篇文章的,也可以直接去看参考文章😄😄,支持原创,我这只是宣传下
  3. 我分成上下 2 篇,会再有一遍单独讲解js 垃圾回收机制
  4. 重点 最后一部分的AST很多人不太清楚 ,重点记录了下来
  5. 还有我们平常用到的babel说的是转化高级js为浏览器识别的代码,但是怎么转呢,都和AST有关

2. JS 代码执行过程

  1. 首先了解js这门语言

1. 编译型语言和解释型语言

  1. 我们知道,机器是不能直接理解代码的。
  2. 所以,在执行程序之前,需要将代码翻译成机器能读懂的机器语言。
  3. 按语言的执行流程,可以把计算机语言划分为编译型语言和解释型语言:
  4. 编译型语言:
    在代码运行前编译器直接将对应的代码转换成机器码,运行时不需要再重新翻译直接可以使用编译后的结果;
  5. 解释型语言:
    需要将代码转换成机器码,和编译型语言的区别在于运行时需要转换。解释型语言的执行速度要慢于编译型语言,因为解释型语言每次执行都需要把源码转换一次才能执行。
  6. Java 和 C++ 等语言都是编译型语言,
    而 JavaScript 是解释性语言,它整体的执行速度会略慢于编译型的语言。
  7. V8 是众多浏览器的 JS 引擎中性能表现最好的一个,并且它是 Chrome 的内核,Node.js 也是基于 V8 引擎研发的。

3. 编译型语言和解释器语言代码执行的具体流程

  1. 流程图 image.png
  2. 在编译型语言的编译过程中,
    编译器首先会依次对源代码进行词法分析、语法分析,
    生成抽象语法树(AST),
    然后优化代码,
    最后再生成处理器能够理解的机器码
    如果编译成功,将会生成一个可执行的文件
    但如果编译过程发生了语法或者其他的错误,
    那么编译器就会抛出异常,最后的二进制文件也不会生成成功。

3.在解释型语言的解释过程中,

同样解释器也会对源代码进行词法分析、语法分析,

并生成抽象语法树(AST),

不过它会再基于抽象语法树生成字节码

最后再根据字节码来执行程序、输出结果


4. V8 执行代码过程

  1. V8 在执行过程用到了解释器和编译器。 其执行过程如下:
  2. Parse 阶段:V8 引擎将 JS 代码转换成 AST(抽象语法树);
  3. Ignition 阶段:解释器将 AST 转换为字节码,解析执行字节码也会为下一个阶段优化编译提供需要的信息;
  4. TurboFan 阶段:编译器利用上个阶段收集的信息,将字节码优化为可以执行的机器码;
  5. Orinoco 阶段:垃圾回收阶段,将程序中不再使用的内存空间进行回收。

这里前三个步骤是JavaScript的执行过程,最后一步是垃圾回收的过程。


下面就先来看看V8 执行 JavaScript的过程。

生成抽象语法树

  1. 生成抽象语法树
  2. 这个过程就是将源代码转换为抽象语法树(AST),并生成执行上下文,执行上下文就是代码在执行过程中的环境信息。
  3. 将 JS 代码解析成 AST主要分为两个阶段:

3.1 词法分析:

这个阶段会将源代码拆成最小的、不可再分的词法单元,称为 token。比如代码 var a = 1;通常会被分解成 var 、a、=1、;

这五个词法单元。

代码中的空格在 JavaScript 中是直接忽略的,简单来说就是将 JavaScript 代码解析成一个个令牌(Token)。

3.2 语法分析:

这个过程是将上一步生成的 token 数据,根据语法规则转为 AST。

如果源码符合语法规则,这一步就会顺利完成。

如果源码存在语法错误,这一步就会终止,并抛出一个语法错误,

简单来说就是将令牌组装成一棵抽象的语法树(AST)。

通过词法分析会对代码逐个字符进行解析,生成类似下面结构的令牌(Token),这些令牌类型各不相同,有关键字、标识符、符号、数字等。代码 var a = 1;会转化为下面这样的令牌

语法分析阶段会用令牌生成一棵抽象语法树,生成树的过程中会去除不必要的符号令牌,然后按照语法规则来生成。


5. AST

1. 基本理解

  1. AST 只是源代码语法结构的一种抽象的表示形式,计算机也不会去直接去识别 JS 代码,转换成抽象语法树也只是识别这一过程中的第一步。
  2. AST 的结构和代码的结构非常相似,其实也可以把 AST 看成代码的结构化的表示,编译器或者解释器后续的工作都需要依赖于 AST。

2. AST的应用场景:

  1. AST 是一种很重要的数据结构,很多地方用到了AST。
  2. 比如在 Babel 中,Babel 是一个代码转码器,可以将 ES6 代码转为 ES5 代码。Babel 的工作原理就是先将 ES6 源码转换为 AST,然后再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JavaScript 源代码。

3.除了 Babel 之外,ESLint 也使用到了 AST。ESLint 是一个用来检查 JavaScript 编写规范的插件,其检测流程也是需要将源码转换为 AST,然后再利用 AST 来检查代码规范化的问题。

  1. 其他场景
    4.1 JS 反编译,语法解析;
    4.2 代码高亮;
    4.3 关键字匹配;
    4.4 代码压缩。

6.   生成字节码

  1. 有了 抽象语法树 AST 和执行上下文后,就轮到解释器就登场了,它会根据 AST 生成字节码,并解释执行字节码。
  2. 字节码就是介于 AST机器码之间的一种代码。
    需要将其转换成机器码后才能执行,字节码是对机器码的一个抽象描述,相对于机器码而言,它的代码量更小,从而可以减少内存消耗。
    解释器除了可以快速生成没有优化的字节码外,还可以执行部分字节码

7.  生成机器码

  1. 生成字节码之后,就进入执行阶段了,实际上,这一步就是将字节码生成机器码。
  2. 一般情况下,如果字节码是第一次执行,那么解释器就会逐条解释执行。在执行字节码过程中,如果发现有热代码(重复执行的代码,运行次数超过某个阈值就被标记为热代码),那么后台的编译器就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码即可,这样提升了代码的执行效率
  3. 字节码配合解释器和编译器的技术就是 即时编译(JIT)。在 V8 中就是指解释器在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。
  4. 因为 V8 引擎是多线程的,编译器的编译线程和生成字节码不会在同一个线程上,这样可以和解释器相互配合着使用,不受另一方的影响。下面是JIT技术的工作机制:
  5. 图示 image.png

  1. 解释器在得到 AST 之后,会按需进行解释和执行。也就是说如果某个函数没有被调用,则不会去解释执行它。
    在这个过程中解释器会将一些重复可优化的操作收集起来生成分析数据,
    然后将生成的字节码和分析数据传给编译器,编译器会依据分析数据来生成高度优化的机器码。
  2. 优化后的机器码的作用和缓存很类似,当解释器再次遇到相同的内容时,就可以直接执行优化后的机器码。当然优化后的代码有时可能会无法运行(比如函数参数类型改变),那么会再次反优化为字节码交给解释器。
  3. 过程图 image.png

执行过程优化

  1. 如果JavaScript代码在执行前都要完全经过解析才能执行,那可能会面临以下问题:
    代码执行时间变长:一次性解析所有代码会增加代码的运行时间。
    消耗更多内存:解析完的 AST 以及根据 AST 编译后的字节码都会存放在内存中,会占用更多内存空间。
    占用磁盘空间:编译后的代码会缓存在磁盘上,占用磁盘空间。
  2. 所以,V8 引擎使用了延迟解析:在解析过程中,对于不是立即执行的函数,只进行预解析;只有当函数调用时,才对函数进行全量解析。
  3. 进行预解析时,只验证函数语法是否有效、解析函数声明、确定函数作用域,不生成 AST,而实现预解析的,就是 Pre-Parser 解析器。

function sum(a, b) {
    return a + b;
}
const a = 666;
const c = 996;
sum(1, 1);

V8 解析器是从上往下解析代码的,当解析器遇到函数声明 sum 时,发现它不是立即执行,所以会用 Pre-Parser 解析器对其预解析,过程中只会解析函数声明,不会解析函数内部代码,不会为函数内部代码生成 AST。

之后解释器会把 AST 编译为字节码并执行,解释器会按照自上而下的顺序执行代码,先执行 const a = 666;  和 const c = 996; ,然后执行函数调用 sum(1, 1) ,这时 Parser 解析器才会继续解析函数内的代码、生成 AST,再交给解释器编译执行。


参考资料

完全参考这篇文章的


初心

我所有的文章都只是基于入门,初步的了解;是自己的知识体系梳理,如有错误,道友们一起沟通交流;
如果能帮助到有缘人,非常的荣幸,一切为了部落的崛起;
共勉
相关文章
|
3天前
|
移动开发 JavaScript 安全
总有一款适合您分享78个JS相册代码
本文分享了78款JS相册代码,包括3D相册旋转木马、图片悬浮效果、倾斜图片幻灯片切换等特效,适用于各种图片展示场景。无论您需要哪种样式,都能在这里找到满意的解决方案。快来挑选吧!参考链接:[点击这里](https://www.vipwb.com/sitemap.xml)。
20 4
|
4天前
|
JavaScript
分享一款520表白节JS代码
今天给大家分享一款JS表白源码 js会随 随机颜色心形跟随鼠标互动520表白节女神表白利器! 修改的话就搜索:LOVEh 就能找到这个英文了。
5 0
分享一款520表白节JS代码
|
11天前
|
JSON JavaScript 前端开发
如何使用代码注释:关于JavaScript与TypeScript
TSDoc是一种标准化TypeScript代码文档注释的规范,使不同工具能无干扰地提取内容。它包括多种标记,如@alpha、@beta等发布阶段标记;@decorator、@deprecated等功能标记;@defaultValue、@eventProperty等描述标记;@example、@experimental等示例与实验性标记;@inheritDoc、@internal等引用与内部标记;@label、@link等链接标记;@override、@sealed等修饰符标记;以及@packageDocumentation、@param、
23 5
|
13天前
|
JavaScript 前端开发 测试技术
如何写高质量的JavaScript代码
在现代Web开发中,JavaScript扮演着至关重要的角色。本文介绍了提升JavaScript代码质量的关键技巧:采用语义化命名增强代码可读性;通过模块化设计提升代码的可维护性和复用性;利用恰当的注释与文档说明代码功能;合理管理全局变量避免命名冲突;实施有效的异常处理增加程序稳定性;并借助工具和框架提高开发效率和代码质量。这些实践共同助力打造高效、可维护的Web应用。代码示例和效果参见相关链接。
17 3
|
9天前
|
Web App开发 JavaScript 前端开发
JavaScript基础知识-使用Firefox进行代码的调试(Debug)
关于如何使用Firefox浏览器进行JavaScript代码调试的基础知识介绍。
24 0
|
11天前
|
开发者 图形学 C#
深度解密:Unity游戏开发中的动画艺术——Mecanim状态机如何让游戏角色栩栩如生:从基础设置到高级状态切换的全面指南,助你打造流畅自然的游戏动画体验
【8月更文挑战第31天】Unity动画系统是游戏开发的关键部分,尤其适用于复杂角色动画。本文通过具体案例讲解Mecanim动画状态机的使用方法及原理。我们创建一个游戏角色并设计行走、奔跑和攻击动画,详细介绍动画状态机设置及脚本控制。首先导入动画资源并添加Animator组件,然后创建Animator Controller并设置状态间的转换条件。通过编写C#脚本(如PlayerMovement)控制动画状态切换,实现基于玩家输入的动画过渡。此方法不仅适用于游戏角色,还可用于任何需动态动画响应的对象,增强游戏的真实感与互动性。
31 0
|
11天前
|
Android开发 iOS开发 C#
Xamarin:用C#打造跨平台移动应用的终极利器——从零开始构建你的第一个iOS与Android通用App,体验前所未有的高效与便捷开发之旅
【8月更文挑战第31天】Xamarin 是一个强大的框架,允许开发者使用单一的 C# 代码库构建高性能的原生移动应用,支持 iOS、Android 和 Windows 平台。作为微软的一部分,Xamarin 充分利用了 .NET 框架的强大功能,提供了丰富的 API 和工具集,简化了跨平台移动应用开发。本文通过一个简单的示例应用介绍了如何使用 Xamarin.Forms 快速创建跨平台应用,包括设置开发环境、定义用户界面和实现按钮点击事件处理逻辑。这个示例展示了 Xamarin.Forms 的基本功能,帮助开发者提高开发效率并实现一致的用户体验。
23 0
|
13天前
|
JavaScript 开发者 UED
Vue.js 错误处理与调试:跟上技术潮流,摆脱开发困扰,成为代码大神不是梦!
【8月更文挑战第30天】在 Vue.js 开发中,错误处理与调试至关重要。本文将对比 Vue 的全局错误捕获机制 `Vue.config.errorHandler` 和组件内 `watch` 监听数据变化的方式,并介绍 Vue 开发者工具、控制台打印 (`console.log`) 以及代码断点 (`debugger`) 等调试方法。此外,还将探讨如何通过自定义错误页面提升用户体验。通过这些技巧的对比,帮助开发者灵活选择适合的策略,确保应用稳定性和开发效率。
34 0
|
13天前
|
JavaScript 前端开发
揭秘Vue.js组件魔法:如何轻松驾驭前端代码,让维护变得轻而易举?
【8月更文挑战第30天】本文探讨了如何利用Vue.js的组件化开发提升前端代码的可维护性。组件化开发将复杂页面拆分为独立、可复用的组件,提高开发效率和代码可维护性。Vue.js支持全局及局部组件注册,并提供了多种组件间通信方式如props、事件等。通过示例展示了组件定义、数据传递及复用组合的方法,强调了组件化开发在实际项目中的重要性。
12 0
|
13天前
|
JSON Dart 前端开发
分享15 个 JavaScript 代码示例及其 Dart 对应代码。
本文对比了React/React Native中的JavaScript语法与Flutter中的Dart语法,帮助开发者快速上手Flutter。内容涵盖JSON处理、数组操作、类型转换、条件判断等常见功能,如`JSON.stringify`与`JsonEncoder().convert`,`array.push`与`list.add`,`parseInt`与`int.parse`等,并提供了15个JavaScript与Dart代码示例对照。这对于从JavaScript转向Dart的开发者尤其有用。
11 0