JS是怎样执行的

1,975 阅读6分钟

JS是怎样执行的

问:请你快速说出JS有哪些标签:
答:单线程、脚本语言、操作DOM吧啦吧啦……
问:好,那你知道JS的执行过程吗?
答:……-_-||

疑问

以前就在想,为什么java需要编译,js就不需要呢?原来编程语言分为静态语言和动态语言:

静态语言:比如C++、Go等,都需要提前编译 (AOT) 成机器码然后执行,这个过程主要使用编译器来完成;
动态语言:比如JavaScript、Python等,只在运行时进行编译执行 (JIT) ,这个过程通过解释器完成;

v8执行js过程

一、【解析器】Parser生成抽象语法树(AST)

在Chrome中开始下载Javascript文件后,Parser就会开始并行在单独的线程上解析代码。生成AST的过程可以分为词义分析和语义分析两个过程。

1、词法分析

主要是将字符流(char stream) 转换成标记流(token stream),字符流就是我们一行一行的代码,token是指语法上不能再分的、最小的单个字符或者字符串。

为了要详细看到token stream,我使用了recast 来模拟浏览器解析JS。

安装esprima

npm i  recast --save

新建文件 recast.js

const recast = require("recast");
const code = 'const LK = 1';
const ast = recast.parse(code);
console.log(ast);

执行node recast.js

node recast.js

我截取了tokens对象,可以看到JS代码被切割为type、value的键值对。

2、语义分析

语义分析的目的是将分词得到的语法单元进行一个整体的组合,分析确定语法单元之间的关系。简单来说,语义分析可以理解成对语句(statement)和表达式(expression)的识别。语义分析是一个递归的过程,所以它会将分词分析出来的数组转化成树形的表达形式(这就是AST中T的由来)。同时,会验证语法,语法如果存在错误的话,会抛出语法错误。

可以看到,短短一句JS代码就被解析为树状的JSON,各位看官可在https://esprima.org/demo/parse.html在线转AST。

二、【解释器】Ignition生成字节码

字节码是机器码的抽象,可以看作是小型的构建块,这些构建块组合到一起构成任何JavaScript功能。字节码比机器码占用更小的内存,这也是为什么V8使用字节码的一个很重要的原因。字节码不能够直接在处理器上运行,需要通过解释器将其转换为机器码后才能执行。

通过上图可以看出,Ignition把前一步得到的AST通过字节码生成器经过一些列的优化生成字节码。 在这个过程中:

  • Register Optimizer: 主要是避免寄存器不必要的加载和存储;
  • Peephole Optimizer: 寻找直接码中可以复用的部分,并进行合并;
  • Dead-code Elimination: 删除无用的代码,减少字节码的大小;

三、【编译器】TurboFan

Ignition执行上一步生成的字节码,并记录代码运行的次数等信息,如果同一段代码执行了很多次,就会被标记为 “HotSpot”(热点代码),然后把这段代码发送给 编译器TurboFan,然后TurboFan把它编译为更高效的机器码储存起来,等到下次再执行到这段代码时,就会用现在的机器码替换原来的字节码进行执行,这样大大提升了代码的执行效率。 另外,当TurboFan判断一段代码不再为热点代码的时候,会执行去优化的过程,把优化的机器码丢掉,然后执行过程回到Ignition

从V8的解析、解释、编译过程去提高js代码性能

一、解析过程

  • 删除多余的代码。
    减少浏览器请求js文件时间,减少解析过程及后续流程时间。
  • 延迟加载不必要的js【优化首屏加载】。
    避免加载和编译那些会延迟页面初始显示的 JavaScript 代码,页面完全加载后,我们可以再开始加载这些功能,以便它们在用户开始交互时立即可用,Google 建议将此延迟加载以 50 毫秒为单位进行,这样就不会影响用户与页面的交互。
  • 正确的书写js代码。
    减少语义分析抛出问题成本。

二、解释过程

V8采用JIT模式编译,JIT必须是强类型语言,编译在执行之前,编译直接生成CPU能够执行的二进制文件,执行时CPU不需要做任何编译操作,直接执行,性能最佳。

  • 声明变量时提供默认类型,加快JIT介入。
  • 不要轻易改变变量的类型,否则提高编译时的类型处理成本。

三、编译过程

  • 始终使用计算复杂度最低的算法和最佳的数据结构来解决任务。

  • 重写算法以获得相同的结果和更少的计算。

  • 避免递归调用。

  • 给重复的函数加入变量、计算和调用。

  • 分解和简化数学公式。

  • 使用搜索数组:用它们来获取基于另一个的值,而不是使用 switch/case 语句。

  • 使条件总是更有可能为真,以更好地利用处理器的推测执行。

  • 如果可以,请使用位级运算符替换某些操作,因为这些运算符的处理周期较短。

AST应用

一、babel

babel是一个javascript编译器,用来将es6语法编译成es5。

1、解析

通过解析器babylon将代码解析成抽象语法树。

2、解析

通过babel-traverse plugin对抽象语法树进行深度优先遍历,遇到需要转换的,就直接在AST对象上对节点进行添加、更新及移除操作,比如遇到箭头函数,就转换成普通函数,最后得到新的AST树。

3、生成(Generate)

通过babel-generator将AST树生成es5代码。

二、vue模板编译过程

Vue 提供了 2 个版本,一个是 Runtime + Compiler ,另一个是 Runtime only 的,前者是包含编译代码的,会把编译的过程放在运行时做,后者是不包含编译代码的,需要借助 webpack 的vue-loader把模板编译render函数。不管使用哪个版本,都有一个环节,就是将模板编译成render函数。

1、解析

将模板字符串解析生成 AST,这里的解析器是vue自己实现的,解析过程中会使用正则表达式对模板顺序解析,当解析到开始标签、闭合标签、文本的时候都会有相对应的回调函数执行,来达到构造 AST 树的目的。

2、解析

vue模板中并不是所有数据都是响应式的,有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在patch的过程跳过对他们的比对。
此阶段会深度遍历生成的 AST树,检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成 DOM 永远不需要改变,这对运行时对模板的更新起到极大的优化作用。

3、生成(Generate)

通过generate方法,将ast生成render函数。

参考链接:juejin.cn/post/684490…