图解 V8 执行 JS 的过程

本文来分享 V8 引擎执行 JavaScript 的过程

1. JS 代码执行过程

在说V8的执行JavaScript代码的机制之前,我们先来看看编译型和解释型语言的区别。

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

    我们知道,机器是不能直接理解代码的。所以,在执行程序之前,需要将代码翻译成机器能读懂的机器语言。按语言的执行流程,可以把计算机语言划分为编译型语言和解释型语言:

    • 编译型语言: 在代码运行前编译器直接将对应的代码转换成机器码,运行时不需要再重新翻译,直接可以使用编译后的结果;
    • 解释型语言: 需要将代码转换成机器码,和编译型语言的区别在于运行时需要转换。解释型语言的执行速度要慢于编译型语言,因为解释型语言每次执行都需要把源码转换一次才能执行。

    Java 和 C++ 等语言都是编译型语言,而 JavaScript 是解释性语言,它整体的执行速度会略慢于编译型的语言。V8 是众多浏览器的 JS 引擎中性能表现最好的一个,并且它是 Chrome 的内核,Node.js 也是基于 V8 引擎研发的。

    编译型语言和解释器语言代码执行的具体流程如下:

    在这里插入图片描述

    两者的执行流程如下:

    • 在编译型语言的编译过程中,编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后优化代码,最后再生成处理器能够理解的机器码。如果编译成功,将会生成一个可执行的文件。但如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。
    • 在解释型语言的解释过程中,同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。
  2. V8 执行代码过程

    V8 在执行过程用到了解释器和编译器。 其执行过程如下:

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

    这里前三个步骤是JavaScript的执行过程,最后一步是垃圾回收的过程。下面就先来看看V8 执行 JavaScript的过程。

    ① 生成抽象语法树

    这个过程就是将源代码转换为抽象语法树(AST),并生成执行上下文,执行上下文就是代码在执行过程中的环境信息。

    将 JS 代码解析成 AST主要分为两个阶段:

    1. 词法分析:这个阶段会将源代码拆成最小的、不可再分的词法单元,称为 token。比如代码 var a = 1;通常会被分解成 var 、a、=、1、; 这五个词法单元。代码中的空格在 JavaScript 中是直接忽略的,简单来说就是将 JavaScript 代码解析成一个个令牌(Token)
    2. 语法分析:这个过程是将上一步生成的 token 数据,根据语法规则转为 AST。如果源码符合语法规则,这一步就会顺利完成。如果源码存在语法错误,这一步就会终止,并抛出一个语法错误,简单来说就是将令牌组装成一棵抽象的语法树(AST)

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

    Keyword(var)
    Identifier(name)
    Punctuator(=)
    Number(1)
    

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

    // 第一段代码
    var a = 1;
    // 第二段代码
    function sum (a,b) {
      return a + b;
    }
    

    将这两段代码分别转换成 AST 抽象语法树之后返回的 JSON 如下:

    • 第一段代码,编译后的结果:
    {
      "type": "Program",
      "start": 0,
      "end": 10,
      "body": [
        {
          "type": "VariableDeclaration",
          "start": 0,
          "end": 10,
          "declarations": [
            {
              "type": "VariableDeclarator",
              "start": 4,
              "end": 9,
              "id": {
                "type": "Identifier",
                "start": 4,
                "end": 5,
                "name": "a"
              },
              "init": {
                "type": "Literal",
                "start": 8,
                "end": 9,
                "value": 1,
                "raw": "1"
              }
            }
          ],
          "kind": "var"
        }
      ],
      "sourceType": "module"
    }
    

    它的结构大致如下:

    在这里插入图片描述

    • 第二段代码,编译出来的结果:
    {
      "type": "Program",
      "start": 0,
      "end": 38,
      "body": [
        {
          "type": "FunctionDeclaration",
          "start": 0,
          "end": 38,
          "id": {
            "type": "Identifier",
            "start": 9,
            "end": 12,
            "name": "sum"
          },
          "expression": false,
          "generator": false,
          "async": false,
          "params": [
            {
              "type": "Identifier",
              "start": 14,
              "end": 15,
              "name": "a"
            },
            {
              "type": "Identifier",
              "start": 16,
              "end": 17,
              "name": "b"
            }
          ],
          "body": {
            "type": "BlockStatement",
            "start": 19,
            "end": 38,
            "body": [
              {
                "type": "ReturnStatement",
                "start": 23,
                "end": 36,
                "argument": {
                  "type": "BinaryExpression",
                  "start": 30,
                  "end": 35,
                  "left": {
                    "type": "Identifier",
                    "start": 30,
                    "end": 31,
                    "name": "a"
                  },
                  "operator": "+",
                  "right": {
                    "type": "Identifier",
                    "start": 34,
                    "end": 35,
                    "name": "b"
                  }
                }
              }
            ]
          }
        }
      ],
      "sourceType": "module"
    }
    

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

    AST的应用场景:

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

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

    除了上述应用场景,AST 的应用场景还有很多:

    • JS 反编译,语法解析;
    • 代码高亮;
    • 关键字匹配;
    • 代码压缩。
    ② 生成字节码

    有了 抽象语法树 AST 和执行上下文后,就轮到解释器就登场了,它会根据 AST 生成字节码,并解释执行字节码。

    在 V8 的早期版本中,是通过 AST 直接转换成机器码的。将 AST 直接转换为机器码会存在一些问题:

    • 直接转换会带来内存占用过大的问题,因为将抽象语法树全部生成了机器码,而机器码相比字节码占用的内存多了很多;
    • 某些 JavaScript 使用场景使用解释器更为合适,解析成字节码,有些代码没必要生成机器码,进而尽可能减少了占用内存过大的问题。

    为了解决内存占用问题,就在 V8 引擎中引入了字节码。那什么是字节码呢?为什么引入字节码就能解决内存占用问题呢?

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

    ③ 生成机器码

    生成字节码之后,就进入执行阶段了,实际上,这一步就是将字节码生成机器码

    一般情况下,如果字节码是第一次执行,那么解释器就会逐条解释执行。在执行字节码过程中,如果发现有热代码(重复执行的代码,运行次数超过某个阈值就被标记为热代码),那么后台的编译器就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码即可,这样提升了代码的执行效率。

    字节码配合解释器和编译器的技术就是 即时编译(JIT)。在 V8 中就是指解释器在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。

    因为 V8 引擎是多线程的,编译器的编译线程和生成字节码不会在同一个线程上,这样可以和解释器相互配合着使用,不受另一方的影响。下面是JIT技术的工作机制:

    在这里插入图片描述

    解释器在得到 AST 之后,会按需进行解释和执行。也就是说如果某个函数没有被调用,则不会去解释执行它。在这个过程中解释器会将一些重复可优化的操作收集起来生成分析数据,然后将生成的字节码和分析数据传给编译器,编译器会依据分析数据来生成高度优化的机器码。

    优化后的机器码的作用和缓存很类似,当解释器再次遇到相同的内容时,就可以直接执行优化后的机器码。当然优化后的代码有时可能会无法运行(比如函数参数类型改变),那么会再次反优化为字节码交给解释器。

    整个过程如下图所示:

    在这里插入图片描述

  3. 执行过程优化

    如果JavaScript代码在执行前都要完全经过解析才能执行,那可能会面临以下问题:

    • 代码执行时间变长:一次性解析所有代码会增加代码的运行时间。
    • 消耗更多内存:解析完的 AST 以及根据 AST 编译后的字节码都会存放在内存中,会占用更多内存空间。
    • 占用磁盘空间:编译后的代码会缓存在磁盘上,占用磁盘空间。

    所以,V8 引擎使用了延迟解析:在解析过程中,对于不是立即执行的函数,只进行预解析;只有当函数调用时,才对函数进行全量解析。

    进行预解析时,只验证函数语法是否有效、解析函数声明、确定函数作用域,不生成 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,再交给解释器编译执行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/380856.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

3.1 Verilog 连续赋值

关键词:assign, 全加器 连续赋值语句是 Verilog 数据流建模的基本语句,用于对 wire 型变量进行赋值。: 格式如下 assign LHS_target RHS_expression ; LHS(left hand side) 指赋值操作…

系统架构24 - 软件架构设计(3)

软件架构风格(上) 概述架构风格数据流架构风格批处理风格管道-过滤风格 调用/返回架构风格主程序/子程序风格面向对象风格层次结构风格客户端/服务器风格 以数据为中心的架构风格仓库风格黑板风格 虚拟机架构风格解释器风格规则系统风格 独立构件架构风格…

Android矩阵Matrix动画缩放Bitmap移动手指触点到ImageView中心位置,Kotlin

Android矩阵Matrix动画缩放Bitmap移动手指触点到ImageView中心位置,Kotlin 借鉴 Android双指缩放ScaleGestureDetector检测放大因子大图移动到双指中心点ImageView区域中心,Kotlin(2)-CSDN博客 在此基础上实现手指在屏幕上点击后&…

leetcode 3027. 人员站位的方案数 II【离散化前缀和+枚举】

原题链接:3027. 人员站位的方案数 II 题目描述: 给你一个 n x 2 的二维数组 points ,它表示二维平面上的一些点坐标,其中 points[i] [xi, yi] 。 我们定义 x 轴的正方向为 右 (x 轴递增的方向)&#x…

Dynamo批量处理多个Revit文件?

Hello大家好!我是九哥~ 最近很多小伙伴都在咨询Dynamo如何批量处理多个Revit文件,之前写过一篇《Dynamo批量修改多文件项目基点参数》,利用的是后台打开Revit的方式,可以实现一些批量操作的功能。 但是这个方法,对于一…

牛客网SQL进阶114:更新记录

官网链接: 更新记录(二)_牛客题霸_牛客网现有一张试卷作答记录表exam_record,其中包含多年来的用户作答试卷记录,结构如下表。题目来自【牛客题霸】https://www.nowcoder.com/practice/0c2e81c6b62e4a0f848fa7693291d…

【Effective Objective - C 2.0】——读书笔记(一)

文章目录 前言一、了解Objective-C语言的起源OC的起源运行期组件和内存管理 二、在类的头文件中尽量少引入其他头文件尽量延后引入头文件或者单独开辟一个文件向前声明 三、多用字面量语法,少用与之等价的方法四、多用类型常量,少用#define预处理指令五、…

如何在 Mac 上恢复永久删除的文件:有效方法

您是否错误地从 Mac 中删除了某个文件,并且确信它已经永远消失了?好吧,你可能错了。即使您认为已永久删除计算机上的数据,仍有可能将其恢复。 在本文中,您将了解如何在 Mac 上恢复永久删除的文件,并了解增…

【开源】JAVA+Vue+SpringBoot实现班级考勤管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统基础支持模块2.2 班级学生教师支持模块2.3 考勤签到管理2.4 学生请假管理 三、系统设计3.1 功能设计3.1.1 系统基础支持模块3.1.2 班级学生教师档案模块3.1.3 考勤签到管理模块3.1.4 学生请假管理模块 3.2 数据库设…

ThinkPad X201 经典小黑 折腾玩

前段时间,在折腾ThinkPad T430时,偶然看到了ThinkPad X200,一个12.1英寸的高端便携小本。 想当年,但那是总裁级别才能用的,应该是接近2万元,我们是一直用DELL的。 没想到的是,在海鲜市场上&am…

【Makefile语法 01】程序编译与执行

目录 一、编译原理概述 二、编译过程分析 三、编译动静态库 四、执行过程分析 一、编译原理概述 make: 一个GCC工具程序,它会读 makefile 脚本来确定程序中的哪个部分需要编译和连接,然后发布必要的命令。它读出的脚本(叫做 …

Java异常的处理 try-catch-finally

目录 什么是异常通过if-else处理异常用if-else堵漏洞的缺点 try-catch例第一种处理第二种处理第三种处理第四种处理 try-catch-finally例 System.exit(0);//终止当前的虚拟机执行 什么是异常 Exception:在程序的运行过程中,发生了不正常的现象&#xff0…

【Larry】英语学习笔记语法篇——换一种方式理解词性

目录 一、换一种方式理解词性 1、名词、形容词、副词,这就是一切 2、词性之间的修饰关系 3、介词其实很简单 形容词属性的介词短语 副词属性的介词短语 ①修饰动词 ②修饰形容词 ③修饰其他副词 一、换一种方式理解词性 1、名词、形容词、副词&#xff0c…

Mysql Day03

多表设计 一对多 在多的一方添加外键约束,关联另外一方主键 一对一 任意一方添加外键约束,关联另外一方主键 多对多 建立第三张中间表,中间表至少包含两个外键,分别关联两方主键 idstu_idcourse_id 1 11 2 12313421524 案…

【Linux】构建模块

🔥博客主页:PannLZ 🎋系列专栏:《Linux系统之路》 🥊不要让自己再留有遗憾,加油吧! 文章目录 构建第一个模块1模块的makefile2内核树内构建3内核树外构建 构建第一个模块 可以在两个地方构建模…

C++——二叉树

引入 map和set特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构 二叉搜索树的特性了解,有助于更好的理解map和set的特性 1.二叉搜索树的概念及优缺点 1.1二叉搜索树的概念 二叉搜索树又称二叉排序树,它或者是一棵空树,或…

网络编程-Socket套接字

目录 1.网络编程 1.1定义与图解 1.2基本概念 (1)发送端和接收端 (2)请求和响应 (3)客户端和服务端 2.Socket套接字 2.1定义 2.2分类 (1)流套接字 (2&#xff…

无人机图像识别技术研究及应用,无人机AI算法技术理论,无人机飞行控制识别算法详解

在现代科技领域中,无人机技术是一个备受瞩目的领域。随着人们对无人机应用的需求在不断增加,无人机技术也在不断发展和改进。在众多的无人机技术中,无人机图像识别技术是其中之一。 无人机图像识别技术是利用计算机视觉技术对无人机拍摄的图像…

黄金交易策略(Nerve Knife):反趋势锁定单的处理机制

锁定单是由大趋势反转之后原来的趋势单转变而来的,会在趋势再次反转时变为趋势单或者转入保留单,也有很大概率在趋势识别到转变之前就被减仓减掉了。 完整EA:Nerve Knife.ex4黄金交易策略_黄金趋势ea-CSDN博客 一、锁定单怎么样来的&#xf…

Git分支常用指令

目录 1 git branch 2 git branch xx 3 git checkout xx 4 git checkout -b xx 5 git branch -d xx 6 git branch -D xx 7 git merge xx(含快进模式和冲突解决的讲解) 注意git-log: 1 git branch 作用:查看分支 示例: 2 git branch xx 作用&a…