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

分享

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 个技巧!

阅读更多

JavaScript 对象 & 原型

前言 这次的 why what or how 主题:JavaScript 对象 & 原型。 此类文章在百度上一搜一大把,其实不用再写了,但是本着把这个问题解释的清清楚楚明明白白,还是开始写了。 原因如下: 1. 面试宝典类文章,弄张图片一糊弄,让人觉得自己理解了。 2. 解释类文章,告诉你一堆语法,对语法一顿解释,告诉你就是这样的,还是没说清楚。 3. 很少有文章单独解释这个点!但这个点是基础!真的很重要! 所以本篇文章想说一说对象 & 原型,但为了确保能顺利理解,请先看完 JS 变量存储?栈 & 堆?NONONO!,因为该篇文章从变量存储的角度来解释 JavaScript 对象 & 原型,请确保看完,在看这篇文章。 什么是对象? 既然要说清楚,

By Breeze

浏览器下的 Event Loop

前言 javascript 是以单线程的形式运行在宿主环境下,javascript 采用了回调的形式来解决异步任务。 为什么是单线程? javascript 的最开始的出现是为了给 web 页面增添一些动态的效果,那么就避免不了获取页面上的元素信息,如果 javascript 是以多线程的形式运行在浏览器内,如果两个线程内的 javascript 同时去获取/修改,某个页面上的元素,那么浏览器该让哪个 javascript 线程拥有获取/修改该元素的权限呢?由于元素的信息会经常性的发生变化,那么又改如何去同步各个线程内所保存的元素信息呢? 所以综合以上问题, javascript 是单线程的原因就显示意见了,单线程在执行时,对于元素信息的引用在同一时间仅可能只有一个,那么以上所有的问题都不存在了。 什么是异步任务? 任何代码在执行时,都会碰到一些需要经过大量时间运算或是等待的代码,在浏览器的环境下,常见的就是 http 任务,比如:资源的加载(图片的 onload 事件),ajax 的请求(XMLHttpRequest 的 onLoad 事件)还有页面元素的点击事件以及

By Breeze

node 下的 Event Loop

前言 通过 浏览器下的 Event Loop ,可以得知 javascript 是一门事件驱动的语言, javascript 主线程通过不断的调用事件队列中的事件来完成异步任务。那么 Node 下是否也是如此? Node 下的事件队列 在 Node 官网有这样一篇文章:The Node.js Event Loop, Timers, and process.nextTick() 。 该文章主要讲述了 Node 中是如何处理以及实现 Node 下的 Event Loop。 主要包含以下内容: 事件队列 不同于浏览器,Node 下的有 6 个事件处理阶段,其执行顺序和队列名称如下: ┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │

By Breeze

什么是 HTML 5?

前言 作为程序员,技术的落实与巩固是必要的,因此想到写个系列,名为 why what or how 每篇文章试图解释清楚一个问题。 这次的 why what or how 主题:现在几乎所有人都知道了 HTML5 ,那么 H5 到底相比于 HTML4 有什么区别呢? 升级版?标准版! HTML5 做为 HTML 标准的第 5 版,于 2014 年发布,相信关注过 HTML5 的发展史的朋友都知道该版本是 WHATWG 和 W3C 握手言和后诞生的,是 W3C 组织与浏览器厂商相互妥协的结果,其中的绝大部分规范都由 WHATWG 组织所制定,之后由 W3C 采纳并入标准中。

By Breeze