【译】JavaScript如何工作的:一览引擎、运行时和调用栈

1,885 阅读4分钟

随着JavaScript(下文简称js)越来越流行,它在各个层面上都留下了身影:前端、后端、hybrid app、嵌入式设备等。

这篇文章是这个系列中第一个深入挖掘js是如何工作的:我们认为理解了js的底层建筑和运行方式可以使我们写出更好的代码和应用。

总览

应该很多人都听过V8引擎这个概念,也知道js是一个单线程的语言,还有它使用了回调队列。

这篇文章里我们会逐一解析每一个概念并解释js是怎么运行的。

如果你是js的初学者,那么这篇文章会让你了解为什么js比起其他语言会那么的“奇怪”。

如果你是js的高手,它会带给你一些关于你每天使用的js运行时是怎样工作的新颖知识。

JavaScript引擎

一个流行的js引擎是谷歌的V8引擎。它被使用在Chrome和Node.js中。这里简单地描述了他是什么样的:

引擎包含量两大部分:

  • 内存堆(Memory Heap)——内存分配的地方
  • 调用栈——这是你的代码执行时栈帧的位置

运行时

在浏览器中有很多被几乎每一位开发者使用的API(比如setTimeout)。这些API,却并不是引擎提供的。

所以,他们来自哪里?

事实上要复杂一点点。

除了引擎以外还有一些东西。我们把这些浏览器提供的东西叫做Web API,比如DOM,AJAX,setTimeout等。

图下方是大名鼎鼎的事件循环(event loop)回调队列( callback queue)

调用堆栈(Call Stack)

Js是一个单线程的语言。所以它也只有一个调用栈。这意味着它同时只能做一件事件。

调用栈是一个数据结构,基本上它记录了我们的程序运行到哪了。如果我们运行进一个函数,那么我们把它放在堆栈的顶部。如果我们从一个函数返回,那么我们弹出(pop off)堆栈顶部的函数。这是堆栈所做的工作。

我们来看一个例子:

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

当我们的引擎刚开始执行上述代码的时候,调用栈会是空。之后的步骤会如下图所示:

调用栈中的每一条都称作栈帧(Stack Frame)

这也解释了当发生异常的时候堆栈轨迹( stack traces)是怎么被建立起来的——其实就是异常发生时调用栈的状态。看下面的列子:

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

如果在Chrome中执行(假设运行的文件叫foo.js),会产生下面的堆栈轨迹:

"Blowing the stack"——这个异常发生在你达到了调用栈最大值的时候。这个很容易出现,特别是在你不小心错误地使用了递归的时候:

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

当引擎开始执行代码,我们会无止尽地执行这个函数。所以这个函数被不断地堆在调用栈上面,就像这样:

这时候浏览器会爆出:

代码运行在单线程的环境是一个很轻松的事,你不必担心一些多线程带来的复杂场景——比如,死锁。

但是单线程也有一些限制。js只有一个调用栈,如何其中一些东西运行很慢怎么办?

并发 & 时间循环

当您在调用栈中行调用需要花费大量时间才能的函数时,会发生什么情况?比如你想在浏览器中进行图像处理。

你可能会问——这为什么会有问题?问题在于调用栈中有函数在执行,浏览器不能做其他的事情。这意味着浏览器不能渲染,不能跑其他代码。这会成为流畅UI界面的阻碍。

而且,一旦你的浏览器要处理太多的任务了,它会失去响应。一些浏览器会采取行动,询问你是否终止这个网页。

所以我们不卡死浏览器且拥有流畅UI的情况下执行大量代码呢?解决方案是异步调用

这会在本系列的下一篇文章中提到:“Inside the V8 engine + 5 tips on how to write optimized code”。(译注:后续翻译尽请关注)

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