JavaScript是如何运行的(上)

847 阅读5分钟

原文地址:blog.sessionstack.com/how-does-ja…

随着JavaScript变得越来越流行,许多团队也开始利用它支持着不同的应用-前端、后端、混合应用程序、嵌入式设备等等。

这篇文章是本系列文章的第一篇,旨在深入研究JavaScript及其实际工作原理:我们认为,通过了解JavaScript的组成部分以及了解它们是如何一起工作的,这将是你能够编写更好的代码和应用程序。

如GitHut stats所示,JavaScript在GitHub中的活跃仓库数和全部提交数量方面处于首位。它在其他类别中也没有落后太多。

image.png

(Check out up-to-date GitHub language stats).

如果你的项目越来越依赖于JavaScript,这意味着开发人员必须利用语言和生态系统提供的一切,对其内部进行更深入的理解,以便构建出高性能和可靠性的应用。


实际上,有很多开发人员每天都在使用JavaScript,但不知道它的背后发生着什么。

概述

几乎每个人都听说过V8引擎的概念,大多数人也都知道JavaScript是单线程的,或者使用的回调队列。

在本文中,我们将详细介绍所有这些概念,并解释JavaScript是如何实际运行的。通过了解这些细节,您将能够编写更好的、无阻塞的应用程序,这些应用程序将适当地利用所提供的api。

如果您对JavaScript还比较陌生,那么这篇文章将帮助您理解JavaScript与其他语言相比为何如此“怪异”。

如果你是一个有经验的JavaScript开发人员,希望它能让你对你每天使用的JavaScript运行时的实际工作有一些新的见解。

JavaScript引擎

我们所知的一个流行的JavaScript引擎是Google的V8引擎。例V8引擎在Chrome和Node.js中使用。下面是一个非常简单的视图:


image.png

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

  • 内存堆-这是执行内存分配的地方
  • 调用堆栈-这是代码执行时堆栈帧的位置

运行时

浏览器中有一些api已经被几乎所有JavaScript开发人员使用(例如“setTimeout”)。但是,这些api不是由引擎提供的。


那么,他们是从哪里来的呢?


事实证明,现实要复杂一些。

image.png


也就是说,我们有引擎,但实际上还有很多东西。我们有一些浏览器提供的叫做Web APIs的东西,比如DOM、AJAX、setTimeout等等。


然后,我们有一个非常流行的事件循环和回调队列。

调用栈

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


调用堆栈是一种数据结构,它基本上记录了我们在程序中的位置。如果我们进入一个函数,我们把它放在堆栈的顶部。如果我们从一个函数返回,我们会弹出堆栈的顶部。这就是堆栈所能做的一切。


让我们看一个例子。请查看以下代码:


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


当js引擎开始执行此代码时,调用堆栈将为空。之后,步骤如下:

image.png

调用堆栈中的每一次入栈称为栈帧。


而这正是抛出异常时构造堆栈跟踪的方式——它基本上是异常发生时调用堆栈的状态。请查看以下代码:


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

如果在Chrome中执行此操作(假设此代码位于名为foo.js的文件中),将生成以下堆栈跟踪:


image.png


“栈溢出”-当达到最大调用堆栈大小时发生这种情况。而且这种情况很容易发生,特别是如果您使用递归而没有对代码进行完善的测试。看看下面的示例代码


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


当js引擎开始执行这段代码时,它首先调用函数“foo”。但是,这个函数是递归的,它开始调用自己而不需要任何终止条件。所以在执行的每一步中,同一个函数都会一次又一次地添加到调用堆栈中。看起来像这样:

image.png


在某些情况下,调用堆栈中的函数调用数超过了调用堆栈的实际大小,浏览器决定通过抛出一个错误来终止操作,该错误可能如下所示:


image.png


在单个线程上运行代码非常容易,因为您不必处理多线程环境中出现的复杂场景—例如死锁。


但是在一个线程上运行也是非常有限的。既然JavaScript只有一个调用堆栈,那么当推栈调用缓慢时会发生什么呢?

并发&事件循环

当调用堆栈中的函数调用需要花费大量时间才能处理时,会发生什么情况?例如,假设您希望在浏览器中使用JavaScript进行一些复杂的图像转换。


你可能会问-为什么这是一个棘手的问题?尽管调用堆栈中有需要被执行的函数,但浏览器实际上无法执行任何其他操作—它被阻塞了。这意味着浏览器不能渲染,不能运行任何其他代码,它被卡住了。这会导致你的应用无法正常流畅的运行。


而且,这还会导致另一个问题。一旦浏览器开始处理调用堆栈中的许多任务,它可能会在相当长的时间内停止响应。大多数浏览器会提出一个错误,询问您是否要终止网页。

image.png

看,这样的场景会导致用户体验非常的糟糕,不是么?


那么,我们如何在不阻塞用户界面和浏览器无响应的情况下执行复杂的代码呢?好吧,异步回调可以解决这个问题。


这将在“JavaScript如何运行的(下)"中得到更详细的解释。