JS工作原理:引擎,运行时和调用堆栈

翻译自: How JavaScript works: an overview of the engine, the runtime, and the call stack

今时今日,JavaScript越来越受欢迎,越来越多地方都可以看到它的身影:前端,服务端,Hybird应用,嵌入设备等等等等。

这一系列文章主要针对JavaScript工作原理的挖掘:我们认为当我们认识JavaScript的组成部分以及了解它们是如何搞在一起的,我们就可以写出更好的代码和应用。

在GitHub的统计可以看得出,JavaScript在很多地方都领先其他语言。
GitHit Range

如果项目越来越依赖JavaScript,意味着作为开发者的我们,更需要更深入的去了解JavaScript的内部工作原理已达到构建更屌的软件的目的。

事实证明,每天都会有很多开发者在用着JavaScript,但是很少人知道它的底层工作原理。

概述

大部分人都只大概听说过V8引擎的概念,或者只知道JavaScript是单线程的和使用着回调队列的。

在这篇文章里,我们会更深入去了解这些概念的细节以及解释JavaScript的时机运行原理。通过了解这些细节,你可以正确使用JS的API写出更好更屌的非阻塞应用。

如果你对JavaScript没有太多认识的话,这篇文章会帮助你了解到为什么JavaScript和其他语言相比会这么的“奇怪”。

如果你是一个有经验的JavaScript开发者,希望可以给予你更多新鲜的知识让你了解到你每天都使用者的JavaScript运行时是怎么样的。

JavaScript引擎

最受欢迎的JavaScript引擎莫过于谷歌家的V8引擎了。这个V8引擎在Chrome和Node.js都使用着。这里有一个非常简单的引擎示意图:
引擎

整个引擎包含两个主要部分:

  • 内存堆 —— 这是内存分配发生的地方。
  • 调用栈 —— 这是你代码执行的栈框架。

运行时(The Runtime)

浏览器提供了很多可供使用的APIs(例如”setTimeout”)。但是,引擎是不会提供这些APIs的。

那么,它们是从何而来?

事实上,它比我们想象中复杂。

引擎

所以,我们仅仅有引擎是不够的。我们还需要由浏览器提供的Web APIs,例如DOM操作,AJAX,setTimeout等等等等。

然后,我们就能拥有有名的事件循环回调队列

调用栈

JavaScript是一个单线程的语言,这意味着它只有一个调用栈。因为它在一个时间内只做一件事。
调用堆栈是一个数据结构,它里面存储的记录是来源于我们的程序。当我们进入一个函数的时候,这个函数就会放置到栈的最顶端。当我们从函数return的时候,我们会把这个函数从栈中pop出来。这就是栈所做的事。

来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18



functionmultiply(x, y) {

return x * y;

}

functionprintSquare(x) {

var s = multiply(x, x);

console.log(s);

}

printSquare(5);

当引擎开始执行这段代码,调用栈是空的,之后的步骤如下图:
例子(1)

调用栈中的每个条目会成为栈帧。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22



functionfoo() {

thrownewError('SessionStack will help you resolve crashes :)');

}

functionbar() {

foo();

}

functionstart() {

bar();

}

start();

通过Chrome来调试会得到下面结果:
例子(2)结果

栈溢出 —— 这会发生在当我们达到了调用栈的最大容量的时候。这其实是很容易发生的,特别是在不对代码进行测试的情况下使用递归,例如:

1
2
3
4
5
6
7
8
9
10



functionfoo() {

foo();

}

foo();

当引擎开始执行这段代码,它会调用一个叫“foo”的函数。然后这个函数同样会调用它自己并且没有结束条件。因此在每个执行步骤都会不断加入同样的函数。调用栈会变成这样:
Overflowing

对于浏览器来说,调用栈中的函数调用次数超过了调用栈的实际大小的时候,浏览器就会抛出一个错误,看起来像这样:
Overflowing2

在单线程开发会感觉更简单,因为你不需要去了解一些多线程环境下才会发生的问题,例如死锁。

但是在单线程运行也是有它的局限性的。因为JavaScript只有一个单一的调用栈,如果某个步骤执行缓慢的时候会发生什么?

并发和事件循环

当调用栈中有占用长时间来执行的函数时会发生什么?例如,想想一下你需要在浏览器中用JavaScript来做复杂的图像变换。

你可能会问 —— 这也是一个问题?这个问题重点在于,当调用栈在执行的时候,浏览器是被堵塞而不能做其他的事的。这意味着浏览器不能渲染,不能运行其他的代码,它就这么卡住了。如果你想要在你的app里面使用漂亮的流体UI的话,这是个大问题。

而且这不是仅有的问题。当你的浏览器开始在调用栈中处理大量的任务的时候,它可能会长时间的停止响应。而对于大部分浏览器来说面对这种情况都会采取行动,主动询问你是否要关掉这个页面。
Close

这个绝对不是好的用户体验。

那么,如何在不阻塞UI使得浏览器无响应的情况下执行繁重的代码呢? 是的,解决方案就是异步回调。