翻译自: How JavaScript works: an overview of the engine, the runtime, and the call stack
今时今日,JavaScript越来越受欢迎,越来越多地方都可以看到它的身影:前端,服务端,Hybird应用,嵌入设备等等等等。
这一系列文章主要针对JavaScript工作原理的挖掘:我们认为当我们认识JavaScript的组成部分以及了解它们是如何搞在一起的,我们就可以写出更好的代码和应用。
在GitHub的统计可以看得出,JavaScript在很多地方都领先其他语言。
如果项目越来越依赖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
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来调试会得到下面结果:
栈溢出 —— 这会发生在当我们达到了调用栈的最大容量的时候。这其实是很容易发生的,特别是在不对代码进行测试的情况下使用递归,例如:
1 |
|
当引擎开始执行这段代码,它会调用一个叫“foo”的函数。然后这个函数同样会调用它自己并且没有结束条件。因此在每个执行步骤都会不断加入同样的函数。调用栈会变成这样:
对于浏览器来说,调用栈中的函数调用次数超过了调用栈的实际大小的时候,浏览器就会抛出一个错误,看起来像这样:
在单线程开发会感觉更简单,因为你不需要去了解一些多线程环境下才会发生的问题,例如死锁。
但是在单线程运行也是有它的局限性的。因为JavaScript只有一个单一的调用栈,如果某个步骤执行缓慢的时候会发生什么?
并发和事件循环
当调用栈中有占用长时间来执行的函数时会发生什么?例如,想想一下你需要在浏览器中用JavaScript来做复杂的图像变换。
你可能会问 —— 这也是一个问题?这个问题重点在于,当调用栈在执行的时候,浏览器是被堵塞而不能做其他的事的。这意味着浏览器不能渲染,不能运行其他的代码,它就这么卡住了。如果你想要在你的app里面使用漂亮的流体UI的话,这是个大问题。
而且这不是仅有的问题。当你的浏览器开始在调用栈中处理大量的任务的时候,它可能会长时间的停止响应。而对于大部分浏览器来说面对这种情况都会采取行动,主动询问你是否要关掉这个页面。
这个绝对不是好的用户体验。
那么,如何在不阻塞UI使得浏览器无响应的情况下执行繁重的代码呢? 是的,解决方案就是异步回调。