JS运行原理

254 阅读13分钟

JS运行原理

解释型和编译型语言

编程语言是写给人看的,但是机器看不懂

所以我们要把我们编写的编程语言转化为机器看的懂得机器语言(01010101)

现在的转化方式有两种:解释编译

编译型语言

可以直接转化为计算机处理器可以执行的机器代码,运行编译型语言需要一个“构建”的步骤,每次代码更新了都需要重新构建,语言执行之前需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件

优点:编译型语言会比解释型语言更快更高效率的执行,也能更好的控制硬件,比如内存管理和CPU使用率

缺点:但是在整个编译的步骤需要花费额外的时间,生成的二进制代码对平台有一定的依赖性

常见的编译型语言:C、C++、Go

此类语言通常都是编译器来编译,系统来执行

解释型语言

通过解释器逐行解释并执行程序的每个命令,每次运行时都需要通过解释器对程序进行动态解释和执行

缺点:因为在运行时翻译代码的过程中增加了开销,解释型语言曾经比编译型语言慢得多,但是随着即时编译的发展,这种差距正在逐渐缩小

优点:但是解释性语言更加灵活,并且一般都能动态植入,程序也比较小,另外,因为是通过解释器自己执行源程序代码的,所以代码本身相对平台是独立的

常见的解释型语言:JS、Python

此类语言都是解释器解释并执行

JavaScript引擎

JS是一种解释型的语言,在源代码执行之前没有被编译成二进制代码,那么计算机是怎么理解和执行纯文本脚本的呢?

JS引擎本质和作用

这里的JS引擎,也就是上面提到的解释器

JS引擎是一个执行JS代码的计算机程序,基本上现代所有浏览器都内置了JS引擎,当我们浏览器中加载到JS文件的时候,JS引擎就会从上到下解析(将其转化为机器代码)并执行文件中的每一行

JS引擎种类

每个浏览器都有自己的JS引擎,最著名的就是**Googlev8**

  • SpiderMonkey:由 Firefox 开发,第一款 JavaScript 引擎,用于Firefox
  • Chakra:由微软开发,用于 Microsoft Edge
  • JavaScriptCore:由苹果开发,用于 webkit 型浏览器,比如 Safari

JS引擎内部

所有的JS引擎内部都包含一个调用栈和一个堆

JS引擎堆栈.png

  • 调用堆栈:是我们代码实际执行的地方
  • 内存堆:这是内存分配发生的地方,是一个非结构化的内存池,存储着我们应用程序需要的所有对象

运行时环境

JS引擎并不能孤立运行,需要有一个运行环境才能发挥更大的作用

例如:Node.js和各个浏览器

这些运行环境往往还会提供如:事件处理,网络请求API,回调队列或消息队列,事件循环等附加功能

JS引擎怎么发挥作用

chrome就是一个JS运行的浏览器环境

chrome是一个多进程的架构,打开一个浏览器会启动多个不同的进程协助浏览器将页面为我们呈现出来

  • 浏览器进程主要负责界面显示、用户交互、子进程管理,同时提供存储等功能
  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下
  • GPU进程:其实,Chrome刚开始发布的时候是没有GPU进程的。而GPU的使用初衷是为了实现3D CSS的效果,只是随后网页、Chrome的UI界面都选择采用GPU来绘制,这使得GPU成为浏览器普遍的需求。最后,Chrome在其多进程架构上也引入了GPU进程
  • 网络进程主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

插件进程主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响

平时常说的浏览器内核,比如webkit内核,就是浏览器的渲染进程,从接受下载文件后再到呈现整个页面的过程,由浏览器渲染进程负责。浏览器内核是多线程的,在内核控制下各线程相互配合以保持同步,一个浏览器内核通常由以下常驻线程组成:

  • GUI渲染线程:负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行,与js引擎线程互斥,防止渲染结果不可预期
  • 定时触发器线程setIntervalsetTimeout所在的线程,浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确,因此通过单独线程来计时并触发是更加合理的方案
  • 事件触发线程:当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理,这些事件可以是当前执行的代码块如定时任务,也可以是来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但是由于JS是单线程的,所以这些事件都得排队等待JS引擎处理
  • 异步http请求线程XMLHttpRequest在连接后通过浏览器新开一个线程请求,将检测到状态变更时,请求完成时,如果设置有回调函数,异步线程就产生状态变更事件放到JS引擎处理队列中等待处理
  • JavaScript引擎线程:,负责处理解析和执行javascript脚本程序,只有一个JS引擎线程(单线程),与GUI渲染线程互斥,防止渲染结果不可预期

注:GUI 渲染线程与 JavaScript 引擎为互斥的关系,当 JavaScript 引擎执行时 GUI 线程会被挂起, GUI 更新会被保存在一个队列中等到引擎线程空闲时立即被执行

JavaScript 是一种单线程编程语言,所以在浏览器内核中只有一个 JavaScript 引擎线程

但是,在 JavaScript 的一个运行环境中,因为可能有多个渲染进程,所以可能有多个 JavaScript 引擎线程

浏览器内核.png

为啥是单线程

原因

作为浏览器的脚本语言,JS的主要用途就是与用户互动,以及操作DOM

这决定了它只能是单线程,否则会带来很复杂的同步问题

比如:现在加入JS同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器就不知道以哪个线程为准了

为啥还有类似多线程的API

为什么有类似WebWorker这样的多线程API?

  • 创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全收主线程控制,而且不能操作DOM)
  • JS引擎线程与Worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)

所以WebWorker并不违背JS引擎是单线程的这一初衷,其主要是用来减轻cpu密集型计算类逻辑的负担

JS代码执行过程

以下是v8引擎对JS代码的解析执行过程

JS解析代码.png

  • 解析器Parser):负责JavaScript 代码转换成 AST 抽象语法树
  • 解释器Ignition):负责AST 转换为字节码,并收集编译器需要的优化编译信息
  • 编译器TurboFan):利用解释器收集到的信息,将字节码转换为优化的机器码

在执行 JavaScript 代码时,首先解析器会将源码解析为 AST 抽象语法树,解释器会将 AST 转换为字节码,一边解释一边执行。然后编译器根据解释器的反馈信息,优化并编译字节码,最后生成优化的机器码,这就是 V8 大体的工作流程

生成AST和执行上下文

分词/词法分析

将字符组成的字符串分解成有意义的代码块这些代码块被称为词法单元(token)

token就是语法上不能再分、最小的单个字符或字符串

例如:var a = 2;会分解成vara=2;,空格是否会被当成词法单元取决于空格在该门语言中是否有意义

解析/词法分析

将第一步生成的词法单元流(数组)转化成一个由元素逐级嵌套所组成的代表了程序语法结构的树,这棵树被称为抽象语法树(AST)

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

例子:编译一个函数

 function add(a, b){
     return a + b;
 }

这个语法块是一个FunctionDeclaration(函数定义)对象

现在将其拆开,其中包含:

  • 一个id,也就是名字add

    id不能继续拆,他是一个最基础的标志对象,作为函数的唯一标志

     {
         name: 'add'
         type: 'identifier'
             ...
     }
    
  • 两个params,也就是参数[a, b]

    可以将[a, b]继续拆,也就是两个identifier组成的数组

     [
         {
             name: 'a'
             type: 'identifier'
             ...
         },
         {
             name: 'b'
             type: 'identifier'
             ...
         }
     ]
    
  • 一块body,也就是函数的主体内容

    body是一个BlockStatement(块状域)对象,用来表示是{return a+b}

    BlockStatement里面还有一个ReturnStatement(return域)对象,用来表示return a+b

    ReturnStatement里面还有一个BinaryExpression(二项式)对象,用来表示a+b

    BinaryExpression拆出来,里面分了三部分:left(a) , operator(+) , right(b)

生成的AST:

AST.jpeg

查看代码AST的工具:recast

 npm i recast -S     #安装recast工具
 //解析代码
 const recast = require('recast')
 const code =
   `
   function add(a, b) {
     return a + b
   }
   `
 const ast = recast.parse(code);
 console.log(ast.program.body[0]);

生成执行上下文

此处已经在juejin.cn/post/713278…文章中已经描述了,此处略

生成字节码

此处由解释器Ignition根据AST生成字节码,并解释执行字节码

一开始V8并没有字节码,而是直接将AST转化成机器码,由于机器码执行效率很高,所以在一段时间内运行效果很好

但是由于后面手机的普及,一开始手机内存小,而V8又需要大量内存来存放转化后的机器码,所以需要引入字节码

字节码是一种介于AST和机器码之间的代码,体积小,需要通过解释器转化为机器码后才能执行

执行代码

在上述过程中,已经生成了字节码,接下来就要执行了

这里要对执行的代码做一个小小的区分:

  • 如果是第一次执行的字节码,解释器Ignition会逐条解释执行
  • 如果发现热点代码,如一段代码重复多次执行,那么编译器TurboFan就会将其编译成高效机器码,再次执行这段代码的时候,就只需要执行机器码即可,大大提升了代码的执行效率

这个过程就是字节码+JIT技术,V8采用的就是这种技术

涉及的其他知识点

词法分析和语法分析

词法分析和语法分析的过程就是发生在解析器执行阶段

词法分析就是将字符序列转化为标记(token)序列的过程

所有token就是源文件中不可在进一步分割的一串字符,类似于英语中的单词,汉语中的词

一般来说程序语言中的 token 有:常数(整数、小数、字符、字符串等),操作符(算术操作符、比较操作符、逻辑操作符),分隔符(逗号、分号、括号等),保留字,标识符(变量名、函数名、类名等)等

语法分析 将这些 token 根据语法规则转换为 AST

在生成 AST 的同时,还会为代码生成执行上下文,在解析期间,所有函数体中声明的变量和函数参数,都被放进作用域中,如果是普通变量,那么默认值是 undefined,如果是函数声明,那么将指向实际的函数对象

字节码和机器码

  • 机器码(machine code):学名机器语言指令,有时也被称为原生码(Native Code),是电脑的 CPU 可直接解读的数据(计算机只认识0和1)
  • 字节码(byte code):是一种包含执行程序、由一序列 OP代码(操作码)/数据对组成的二进制文件。字节码是一种中间码,它比机器码更抽象,需要直译器转译后才能成为机器码的中间代码

相比机器码,字节码不仅占用内存少,而且生成字节码的时间很快,提升了启动速度

随着即时编译的发展,解释型语言和编译型语言的运行速度的差距正在缩小

同时采用了解释执行和编译执行这两种方式,这种混合使用的方式就称为 JIT (即时编译) ,V8 采用的就是这种技术

在解释器执行字节码的过程中,如果发现有热点代码,比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率

ES和JS引擎的关系

  • ES指的是JS的语言标准及语言版本
  • JS引擎的核心就是实现ES标准,此外还提供一些额外的机制(例如v8的引擎)

调用堆栈的执行过程

JS是一种单线程的编程语言,这意味着它有一个调用堆栈,一次只能做一件事

调用堆栈是一种数据结构,基本记录了我们在程序中的位置

如果我们执行一个函数,他会放在栈顶,执行完之后,就会从栈顶弹出

调用堆栈.gif

调用堆栈中的每一个条目被称为堆栈帧

当调用堆栈中的一个 堆栈帧 需要大量时间才能被处理时,就会产生卡顿,因为浏览器没法做其他事情了

性能优化

一般我们将JS的优化中心聚焦在单次脚本执行时间和脚本的网络下载上:

  • 提升单次脚本的执行速度,避免JS的长任务霸占主线程
  • 避免大的内联脚本,因为解析HTML时,解析和编译也会占用主线程
  • 减少JS文件的容量,小文件会提升下载速度,并且占用更低的内存

相关文章