JavaScript的工作机制

v8引擎,js的内存管理,执行上下文

Posted by AzirKxs on 2022-07-12
Estimated Reading Time 9 Minutes
Words 2.7k In Total
Viewed Times

注意:本篇文章不完善,有很多坑,原因是我调查了很长时间对这些概念仍然很模糊,这篇文章算是给自己挖的一个坑,等待我后续的完善

参考

作者:小烜同学 链接:https://juejin.cn/post/6844903512237670414

作者:子非 链接:https://juejin.cn/post/6844903682283143181

前言

学习目的:前两篇文章我们了解了微任务与宏任务,浏览器的运行方式以及事件循环机制。我们了解到JavaScript是单线程的,如何写出高效率的代码成了一个问题。通过了解JavaScript的运行机制我们可以利用这些提供的 API 来写出更好的,非阻塞的应用来。

这篇文章介绍v8引擎,Javascript的调用栈,执行上下文的概念

浏览器的组成

首先注意区分浏览器的组成和浏览器的进程,不要混淆

  1. 用户界面:
    用户界面主要包括:地址栏,后退/前进按钮,书签目录等;(除了从服务器请求到的网页窗口)

  2. 浏览器引擎:
    用来查询及操作渲染引擎的接口;

  3. 渲染引擎:
    用来显示请求的html内容;(包括样式,图片,js)

  4. 网络:
    主要是来完成网络调用,例如http请求,它具有平台无关的接口,可以在不同平台上工作;

  5. UI后端:
    用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口。

  6. JS解释器 :
    用来解释执行JS代码;(v8引擎)

  7. 数据存储:
    属于持久层,浏览器需要在硬盘中保存类似cookie的各种数据,HTML5定义了web database技术,这是一种轻量级完整的客户端存储技术;

v8引擎简介

1-3

当今最流行的Javascript引擎就是chrome浏览器的V8引擎,这个引擎也应用于node.js,这个引擎主要由两个部分组成,内存堆和调用栈。

  • 内存堆:内存分配发生的地方
  • 调用栈:代码执行的地方

ps:最初浏览器内核的概念包括渲染引擎与JS引擎,目前习惯直接称渲染引擎为内核,JS引擎独立。v8的js引擎就是独立的

v8引擎有两个编译器

  • full-codegen — 一个简单并且速度非常快的编译器,可以生成简单但相对比较慢的机器码。
  • Crankshaft — 一个更加复杂的 (即时) 优化编译器,生成高度优化的代码。

v8引擎在内部使用了多个线程

  • 主线程完成你所期望的任务:获取你的代码,然后编译执行
  • 还有一个单独的线程用于编译,以便主线程可以继续执行,而前者就能够优化代码
  • 一个 Profiler (分析器) 线程,它会告诉运行时在哪些方法上我们花了很多的时间,以便 Crankshaft 可以去优化它们
  • 还有一些线程处理垃圾回收扫描

当第一次执行 JavaScript 代码的时候,V8 利用 full-codegen 直接将解析的 JavaScript 代码不经过任何转换翻译成机器码。这使得它可以非常快速的开始执行机器码,请注意V8不使用任何中间字节码表示,从而不需要解释器。
当你的代码已经运行了一段时间了,分析器线程已经收集了足够的数据来告诉运行时哪个方法应该被优化。
然后, Crankshaft 在另一个线程开始优化。它将 JavaScript 抽象语法树转换成一个叫 Hydrogen 的高级静态单元分配表示(SSA),并且尝试去优化这个 Hydrogen 图。大多数优化都是在这个级完成。

JavaScript的内存管理

根据我之前的学习来说:基本数据类型保存在栈内存中,因为基本数据类型占用空间小、大小固定,通过按值来访问,属于被频繁使用的数据,而引用数据类型存放在堆中,栈中存放的是引用。然而,事实貌似并不是这样的,这里有两篇文章https://www.zhihu.com/question/482433315/answer/2083349992,https://juejin.cn/post/6844903997615128583

我要给这个问题打上一个问号,因为我还没有想明白……还需要调查

执行上下文

执行上下文是评估和执行javascript代码运行环境的抽象概念,当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

执行上下文的类型

  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。

调用栈(执行栈)

JavaScript 是一门单线程的语言,这意味着它只有一个调用栈,因此,它同一时间只能做一件事。如果我们运行一个函数,就会先将一个函数放到栈顶,当这个函数返回的时候从栈顶弹出。

理解调用栈的案例:

1
2
3
4
5
6
7
8
9
10

function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);

函数的执行过程如下图:

1-1

理解执行上下文的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let a = 'Hello World!';

function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}

function second() {
console.log('Inside second function');
}

first();
console.log('Inside Global Execution Context');

函数的执行过程如下图:

1-4

  1. 当上述代码在浏览器加载时,JavaScript 引擎创建了一个全局执行上下文并把它压入当前执行栈。当遇到 first() 函数调用时,JavaScript 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。
  2. 当从 first() 函数内部调用 second() 函数时,JavaScript 引擎为 second() 函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当 second() 函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即 first() 函数的执行上下文。
  3. 当 first() 执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。

栈溢出

接下来看一段代码:

1
2
3
4
5
6

function foo(){
foo();
}
foo();

当我们的引擎开始执行这段代码的时候,它从 foo 函数开始。然后这是个递归的函数,并且在没有任何的终止条件的情况下开始调用自己。因此,每执行一步,就会把这个相同的函数一次又一次地添加到调用堆栈中。然后它看起来就像是这样的:

1-2

如何创建执行上下文

在javascript执行之前,执行上下文将经历创建阶段和执行阶段

创建阶段

  1. this 值的决定,即我们所熟知的 This 绑定。

    • 在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this引用 Window 对象)。在函数执行上下文中,this 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined(在严格模式下)。
  2. 创建词法环境组件。

    • 词法环境是一种持有标识符—变量映射的结构(这里的标识符指的是变量/函数的名字,而变量是对实际对象或原始数据的引用)

    词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用。

    • 环境记录器是存储变量和函数声明的实际位置。
    • 外部环境的引用意味着它可以访问其父级词法环境(作用域)。

    这里需要做一个全局环境和函数环境的区分:

    • 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this的值指向全局对象。
    • 在函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

    环境记录器也有两种类型:

    • 声明式环境记录器存储变量、函数和参数。
    • 对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。

    也就是说:
    在全局环境中,环境记录器是对象环境记录器。
    在函数环境中,环境记录器是声明式环境记录器。

  3. 创建变量环境组件。
    它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
    如上所述,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。
    在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。

太逆天了,这篇文章写的乱七八糟,涉及到了三个概念,浏览器的v8引擎,js的内存管理,执行上下文,而且对于前两个概念来说还很模糊,文章很少,每一个都可以深挖。


如果这篇文章对你有帮助,可以bilibili关注一波 ~ !此外,如果你觉得本人的文章侵犯了你的著作权,请联系我删除~谢谢!