JavaScript执行机制、事件循环
徐徐 抱歉选手

JavaScript 引擎与运行时

JavaScript代码的运行,需要有JavaScript Engine与JavaScript Runtime。

JavaScript Engine

主流的JavaScript引擎有V8,SipderMonket,JavaScriptCore等。谷歌开发的V8引擎,用于Node和Chrome浏览器中。

JavaScript引擎就是主要负责将顶层的JS代码编译为不同CPU对应的汇编代码。同时还能够执行JavaScript代码,完成内存分配与垃圾回收等功能。

为什么需要把JS代码转换为汇编语言?

一方面,CPU只认识自己的指令集,如rm/jmp/sub这些,不可能叫每一个程序员都去写汇编语言。

另一方面,不同CPU架构的CPU,如Intel,ARM以及MIPS等,指令集是不一样的。

JavaScript RunTime

JavaScript运行时给JavaScript Engine提供了各种与外界交互的API/对象与机制,如AJAX请求、设置定时器、响应事件等操作。虽然node和chrome浏览器都使用了V8引擎,但是他们的运行时是不同的。

V8引擎

V8引擎主要由两个部件组成:

  • Memory Heap/内存堆:分配内存地址的地方
  • Call Stack/调用栈:执行代码的地方

  • JavaScript Runtime

    V8引擎缺乏与外部交互的能力,无法进行ajax请求、设置定时器等操作,Javascript运行时为Javascript提供对象和机制,使他能够和外界交互。

    这里的运行时就是我们常说的运行环境,即浏览器或Node.js环境。

V8工作原理

V8由许多模块构成,其中以下四个是最重要的

  • Parser:负责将JavaScript源码转换为Abstract Syntax Tree(AST)
  • Ignition:interpreter,即解释器,负责将AST转换为Bytecode,解释执行Bytecode;同时收集编译器TurboFan优化编译需要的信息(如函数参数的类型)。(AST->Bytecode)
  • TurboFan:compiler,即编译器,利用Ignition解释器所收集的类型信息,将Bytecode转换为优化的汇编代码。(Bytecode->machine code)
  • Orinoco:garbage collector,即垃圾回收模块,负责将程序不再需要的内存空间回收。

V8引擎各个模块之间的关系如下:

2019-07-16-ignition-turbofan-pipeline

V8引擎的工作流程如下:

V8引擎工作原理

Ignition如果要收集优化编译的各种信息,这要求函数至少要执行一次,那么Ignition才能将收集到的函数信息传给TurboFan;被多次调用的函数会被识别为热点函数,被多次调用意味着Ignition可以收集到参数类型信息协助TurboFan优化编译,这个时候TurboFan就能将Bytecode编译为Optimized Machine code;如果函数一次都没有被调用,那么这个函数就不会被编译。

上图中的红线部分由Optimized Machine Code转到Bytecode,这个过程叫Deoptimization。这是因为Ignition收集的信息有可能是错误的,因为JavaScript是动态类型语言,传入的参数类型在函数执行之前是不确定的,虽然有可能一开始传入了整数类型,Ignition也收集了信息传给了TurboFan,但是下一次传入了字符串类型,之前生成的Optimized Machine Code就是不合适的。

V8引擎在5.9版本之前,是没有生成字节码的过程的,而是直接将AST通过Full-codegen快速生成为优化的机器码,再通过Crankshaft对热点函数优化编译。直接转换机器码,会导致机器码占用空间过大,无法一次性编译全部代码,而且有的代码只运行一次,浪费内存资源。引入生成字节码的过程后,一方面,字节码比机器码占用更少的空间,一次性全部编译代码;另一方面,实际上Bytecode就是没有特定CPU的汇编语言,Bytecode的生成无需考虑各种各样的CPU,提高了V8引擎的可扩展性。

代码块

JavaScript代码的运行分为编译阶段和执行阶段。执行阶段不是一行一行解释执行,而是先划分代码块,按照代码块执行。目前有三类代码块:

  • Function code
  • Global code
  • Eval code

执行上下文/执行环境

从内存堆被放到调用栈是每一个代码块都有的基本执行环境,就是执行上下文/execution context。

任何代码的运行,总会在调用栈中先创建全局执行环境,再次基础之上创建函数执行环境;执行环境创建完毕之后,对执行环境中的环境变量赋值,再执行代码具体内容。

执行上下文的运行分为两个阶段:创建阶段和执行阶段。

三类执行上下文

对应到代码块,就有三类执行上下文:

  • Global execution context:基础上下文,任何不再函数内部的代码都在全局上下文中。一个程序中只有一个全局执行上下文。创建一个全局的Window对象,并设置this的值等于这个全局对象。
  • Function execution context:每一个函数在被调用时,都会创建一个新的执行上下文。
  • Eval execution context:执行在Eval内部的代码的上下文。

执行上下文的创建

执行上下文的创建阶段确定如下三个部分:

  • 该执行上下文的this是什么
  • 创建词法环境 Lexical Environment

  • 创建变量环境 Variable Environment

this确定

优先级从低到高依次为:

  • 默认this指向全局对象

  • 对象内部绑定外部全局函数,则this指向对象。

  • 显式绑定对象 call apply bind等方法

  • new调用构造函数生成对象

词法环境

词法环境由

  • 环境记录器/Environment Record:用于储存变量和函数声明的实际位置。可以分为声明式环境记录器decarative与对象式环境记录器object。

  • 外部环境的引用/outer Lexical Environment:使当前词法环境可以访问父级词法环境(作用域)。

组成。

词法环境可以分为

  • 全局环境:环境记录器储存内建函数Object/Array等、用户定义的全局变量、原型函数、全局对象等。外部环境的引用为null。
  • 模块环境:外部环境的引用为全局环境。
  • 函数环境:环境记录器储存函数内部用户定义的变量。外部环境的引用可以是其他函数的内部词法环境(嵌套函数),也可以是全局词法环境。

这三类。

变量环境

词法环境中储存的是函数声明和变量(let/const)的绑定,而环境变量用来储存var变量的绑定。

事件循环机制

参考

JavaScript深入浅出第4课:V8引擎是如何工作的?

V8引擎工作机制

JavaScript运行机制:事件驱动编程详解

深入浅出Javascript事件循环机制(上)

深入浅出Javascript事件循环机制(下)

JavaScript的运行机制

  • 本文标题:JavaScript执行机制、事件循环
  • 本文作者:徐徐
  • 创建时间:2021-04-04 11:13:56
  • 本文链接:https://machacroissant.github.io/2021/04/04/js-principle/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论