kekobin / blog

blog

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

JavaScript 引擎运行原理

kekobin opened this issue · comments

HTML解析器遇到script => 请求脚本 => 将请求的脚本作为字节流,由字节流解码器负责。字节流解码器在下载字节流时对其进行解码。

字节流解码器从已解码的字节流中创建令牌。例如,0066解码为f, 0075到u, 006e到n, 0063到c, 0074到t, 0069到i, 006f到o, 006e到n,后面跟一个空格。就像JS中的function,这是 JS 中的一个保留关键字,它会创建一个标记,并将其发送给解析器。对于字节流的其余部分也是如此。

该引擎使用两个解析器:预解析器(pre-parser)和解析器(parser)。预解析器只提前检查标记,以查看是否有语法错误。这可以减少发现代码中的错误所需的时间,否则解析器稍后就会发现这些错误。
如果没有错误,解析器将根据从字节流解码器接收到的标记创建节点。使用这些节点,它创建了一个抽象语法树,即AST。

接下来,轮到解释器(interpreter)了。遍历AST并根据AST包含的信息生成字节码的解释器。一旦字节码完全生成,AST就会被删除,从而清除内存空间。最后,生成的机器码就可以在电脑上运行了。

虽然字节码很快,但它可以更快。当这个字节码运行时,将生成信息。它可以检测某些行为是否经常发生,以及所使用数据的类型。也许已经调用一个函数几十次了:现在是时候优化它了,这样它会运行得更快!

字节码与生成的类型反馈一起发送到优化编译器(ptimizing compiler)。 优化的编译器接收字节码和类型反馈,并根据这些信息生成高度优化的机器码。

JS 是一种动态类型语言,这意味着数据类型可以不断变化。如果 JS引擎每次都要检查某个值的数据类型,那么速度会非常慢。

相反,JS 引擎使用一种称为**内联缓存(inline caching)**的技术。它将代码缓存在内存中,希望将来它会以相同的行为返回相同的值.假设某个函数被调用100次,并且到目前为止总是返回相同的值。它将假设在第101次调用它时也会返回这个值。

假设我们有以下函数sum,(到目前为止)每次都使用数值作为参数来调用它:

function sum(a, b) {
return a + b
}
sum(1,2)
执行结果为 3。 下次调用它时,它将假定我们再次使用两个相同数字对其进行调用。

如果假投,那么就不需要动态查找,只需要使用存储在特定内存槽中的结果,该槽已经有一个引用。否则,如果假设不正确,它将反优化代码并恢复到原始字节码,而不是优化后的机器码。

例如,下一次调用它时,我们传递的是字符串而不是数字。因为 JS 是动态类型的,所以这样做不会有任何错误。

function sum(a, b) {
return a + b
}
sum('1',2)
这意味着数字2将被强制转换成字符串,而函数将返回字符串'12'。它返回执行解释的字节码并更新类型反馈。

JS引擎中也有堆(Memory Heap)和栈(Call Stack)的概念

  • 栈。用来存储方法调用的地方,以及基础数据类型(如var a = 1)也是存储在栈里面的,会随着方法调用结束而自动销毁掉(入栈-->方法调用后-->出栈)。
  • 堆。JS引擎中给对象分配的内存空间是放在堆中的。如var foo = {name: 'foo'} 那么这个foo所指向的对象是存储在堆中的。
function foo () {
  var x; // local variables
  var y; // captured variable, bar中引用了y

  function bar () {
  // bar 中的context会capture变量y
    use(y);
  }

  return bar;
}

如上述情况,变量y存在与bar()的闭包中,因此y是captured variable,是存储在堆中的。

V8 引擎包含两个主要组件:

  • 内存堆 —— 进行内存分配
  • 调用栈 —— 代码执行/栈帧

运行时 (Runtime)
几乎所有 JavaScript 开发人员都使用过浏览器中的 APIs (e.g. setTimeout) 。�但是这些 �APIs 不是由 JavaScript 引擎提供的。
所以,它们从何而来?
image
由浏览器提供的叫做 Web APIs 的东西,比如 DOM, AJAX, setTimeout 等等。
然后,我们还有 事件循环 (event loop) 和 回调队列 (callback queue) 。

调用栈 (Call Stack)

function multiply(x, y) {
  return x * y
}

function printSquare(x) {
  var s = multiply(x, x)
  console.log(s)
}

printSquare(5)

当� JavaScript 引擎开始执行代码时,调用栈是空的。之后的步骤如下:
image
当抛出异常可以看到堆栈追踪是如何构造的 —— 当发生异常时�,它就是调用栈的状态。�看下面的代码:

function foo() {
  throw new Error('SessionStack will help you resolve crashes:')
}

function bar() {
  foo()
}

function start() {
  bar()
}

start()

如果在 Chrome 中执行 (假设是 foo.js 中的代码) ,将会生成下面的堆栈追踪:
image

“堆栈溢出 (Blowing the stack)“ —— 当达到最大调用栈大小的时候发生。这很容易发生,特别是如果你使用递归但没有全面的测试。看下面的示例代码:

function foo() {
  foo()
}

foo()

当 JavaScript 引擎开始执行这段代码,开始调用 foo 函数。但是在没有终止条件的�情况下 foo �会递归的调用自己。所以每执行一步,就会把这个相同的函数一次又一次的添加到调用栈中。�看起来像下面这样:
image

但是,�在某一时刻,调用栈中的函数调用数量超过了调用栈的实际大小,浏览器通过抛出一个错误来结束它。如下所示:
image