总览:工作引擎、运行时与调用栈

1,121 阅读6分钟

console.info

本文翻译自 sessionstack.com,该系列共 19 篇文章,解释了 JavaScript 是如何在浏览器中工作,深入到了 JavaScript 解释器的工作原理,以及 JavaScript 技术实现细节。

原文链接: How JavaScript works: an overview of the engine, the runtime, and the call stack

前言

伴随着 JavaScript 越来越流行,越来越多的领域都能看到它的身影比如:前端、后端、APP 混合应用程序、嵌入式设备等。

该篇文章是本系列文章的第一篇,旨在深入探讨 JavaScript 及其工作方式。我们认为:通过了解 JavaScript 的组成以及这些组成如何一起工作,能够写出更好的代码和应用。我们还将分享构建 SessionStack 网站时的一些经验和代码原则,SessionStack 网站是一个轻量级的 JavaScript 应用,为了保持网站竞争力,它的代码强大且高效。

根据 GitHut 统计信息所示(下图),以目前仍在维护的项目和 GitHub 中各类语言的推送总数而言,JavaScript 排名第一。

githut 上各语言排行

当然你也可以查看最新的数据排行

由此可见越来越多的项目开始依赖 JavaScript,这意味着开发者必须能够利用 JavaScript 生态提供的内容,并且对其内部结构需要更加深入的了解,才能构建出优秀的软件。

但就目前而言,很多开发者每天都在使用 JavaScript,却不知道这背后发生了什么。

总览

几乎每个使用过 JavaScript 的开发者都听说过 V8 引擎,其中大部分也知道 JavaScript 以单线程的形式运行在浏览器中,或明白 JavaScript 使用回调的方式来处理任务。

在这篇文章中,我们将详细介绍这些概念,并解释 JavaScript 是如何运行的。相信了解这些细节后,大家将能够利用宿主环境(浏览器或是 node )下提供的 API 编写更好、非阻塞的应用。

如果你是 JavaScript 新手,那么该篇文章将助你了解:为何 JavaScript 与其他语言相比如此 “古怪” ?

如果你是一位经验丰富的 JavaScript 开发人员,希望它能带给你对 JavaScript 运行时新的见解。

Engine 引擎

谷歌的 V8 引擎是最常见的 JavaScript 引擎,V8 引擎被用在 Chrome 以及 Node.js 中,下图是 V8 引擎的构成(简化版):

V8 引擎的基础实现

该引擎主要由两部分组成:

  1. Memory Heap(内存堆):发生内存分配回收的地方(变量的创建及销毁)
  2. Call Stack(调用栈):代码执行的地方(函数的调用)

Runtime 运行时

在浏览器中,有些经常被开发者使用到的 API(比如 setTimeout),这些 API 并非由 JavaScript 引擎提供。

那么,这些 API 哪儿来的?

这说起来有点复杂。

下图的 Web APIs 都是这类 API,一个完整的 JavaScript 运行时如下图所示:

JS 运行时所包含的内容

因此,仅仅拥有 JavaScript 引擎还不足以完成所有的任务,除了引擎提供的 API 我们还拥有 Web APIs,这些 API 由浏览器提供,比如 DOM 操作、AJAXsetTimeout 等等。

当然,一个完整的运行时,还包括了 Event Loop(事件循环)以及 Callback Queue(事件队列)。

Call Stack 调用栈

JavaScript 是单线程语言,这意味着它只拥有一个调用栈(Call Stack),因此同一时间,它只能干一件事。

调用栈是一种数据结构,它记录程序执行到的函数。如果程序进入某个函数,那么 JavaScript 引擎将生成一个调用帧并将它压入栈顶,如果程序从这个函数返回,那么该函数对应的调用帧便会从栈顶弹出,这就是调用栈所能做的一切。

我们通过一个简单的例子来了解这个过程:

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

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

printSquare(5);

JavaScript 引擎开始执行代码时,调用栈是空的,接着就会发生下图所示内容:

JS 调用栈

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

异常

当发生异常时,调用栈将被保存,这也是显示异常信息最基本的条件。我们来看下面的代码:

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

function bar() {
    foo();
}

function start() {
    bar();
}

start();

当上述代码运行在 Chrome 时,将会发生如下图的错误提示:

发生错误时 JS 的调用信息

栈溢出

栈溢出 - 当程序的调用栈达到一定程度后,JavaScript 引擎便会报出栈溢出的错误。这很容易发生,尤其当你使用递归编写代码但却不进行大量测试时。以下代码就是一个例子:

function foo() {
    foo();
}

foo();

JavaScript 引擎开始执行这段代码时,它首先调用函数:foo,但这个函数递归且在没有任何终止条件的情况下调用了自己。这导致的结果就是:在执行的每个步骤中,相同的函数被一次又一次地添加到调用栈中,从而导致栈溢出。就如下图所示:

JS 调用栈溢出

在浏览器中,当发生栈溢出时,会导致程序出错,就如下图所示:

JS 调用栈溢出示例

线程

在单线程的环境下运行代码很简单,因为不需要处理多线程场景下会发生的问题,比如:死锁。

但是单线程的运行环境也有所限制,由于 JavaScript 仅拥有一个调用栈,当某段代码运行变慢时,会发生什么?

并发 & Event Loop(事件循环)

当在调用栈中的函数调用要花费大量时间才能完成时,会发生什么?比如,你想在浏览器中使用 JavaScript 进行一些复杂的图像转换。

你可能会问:这也是一个问题?

试着想想:当调用栈里有函数需要执行,但浏览器却不能做任何事(因为它被阻塞了)。这意味着浏览器不能进行页面渲染,不能运行任何其他代码,它卡住了。这导致的问题就是:页面不能顺畅的渲染,给了使用者一个糟糕的体验。

但这并不是唯一的问题:当浏览器开始处理调用栈里如此多的任务时,它可能会停止响应很长时间,绝大部分的浏览器都会发出一个警告,询问使用者是否终止这个目前无法响应的页面,就如下图一样:

页面无响应弹框

这并不是一个良好的用户体验!!

那么,我们该如何才能在不阻塞 UI 或照成浏览器无响应的情况下执行大量代码?

异步回调!!

这部分内容将在该系列的下篇文章中进行详细的解释:深入 V8 引擎 & 编写优化代码的 5 个技巧!