阅读 2162

JS引擎、运行时与调用栈概述

概览

几乎所有人都已经听说了V8引擎的概念,大多数人都知道JavaScript是单线程运行的或者说是使用回调队列的。

接下来,我们将详细的讲述这些概念,解释JavaScript到底是怎样运行的。当知道了这些细节后,你就能合理利用已有的API写出更好的,非阻塞的应用。 如果你是JavaScript新手,这篇博客可以帮助你理解为什么相对于其他语言,JavaScript显得如此奇怪。

如果你是比较有经验的JavaScript开发者,希望这篇博客可以让你对你每天使用的JavaScript运行时到底是怎样运行的有一些新的见解。

JavaScript引擎

一个流行的JavaScript引擎是谷歌的V8引擎。例如在Chrome和Node.js中使用的就是V8引擎。下图是V8引擎一个非常简单的预览:

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

  • Memory Heap--内存分配区

  • Call Stack--代码运行时栈

运行时

大部分JavaScript开发者都使用过浏览器的API(例如“setTimeout”)。然而这些API都不是由引擎提供的。 那么,它们来自哪里呢? 真实情况有点复杂。

所以除了引擎还有喝很多其他的东西。有浏览器提供的Web API,像DOM,AJAX,setTimeout等等。 然后还有非常有名的event loop和call queue。

调用栈

JavaScript是一门单线程的编程语言,也就是说它只有一个调用栈,因此它只能一次做一件事。

调用栈是一个记录程序运行到哪里的数据结构。调用函数的时候,我们会把它放到栈的最顶部。从函数返回的时候,我们会把它从栈的最顶部弹出来。这就是调用栈做的所有的事情。

我们来看一个例子,看一下如下代码:

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

当引擎开始执行这段代码的时候,调用栈是空的。接下来,每一步如下所示:

每次进入调用栈成为栈桢。 这就是当一个异常抛出时,栈的记录是怎样组成的,基本上就是当一个异常发生的时候调用栈的状态。看一下如下代码:

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

如果是运行在Chrome中(假定这段代码在foo.js文件中),将会生成如下栈记录:

“栈溢出”--这个发生在超过调用栈最大空间的时后。这非常容易发生,特别是当你使用递归但又没有非常严格的测试你的代码的时候。看一下如下代码示例:

function foo() {
    foo();
}
foo();
复制代码

当引擎开始执行这段代码的时候,首先调用“foo”函数,但是这个函数是递归的,开始调用自己并且没有结束条件。所以每一步执行,相同的函数都会一遍又一遍的加入到调用栈中,看上去就像这样:

然而在某个时间点上调用栈中的函数调用数量将会超过调用栈的实际大小,此时浏览器决定采取行动,抛出一个错误,我们就会看到像下面这样的提示:

在单线程上运行代码是非常容易的,你不用处理在多线程中发生的复杂的场景--例如死锁。

Concurrency & the Event Loop

在调用栈中存在需要花费很多时间的函数调用时会发生什么呢?例如,想象一下你需要在浏览器中利用JavaScript来做一些复杂的图片转换。

你可能会问--这有什么好问的?问题就是调用栈在执行函数的时候,浏览器不能做其他的事--浏览器被阻塞了。这意味着浏览器将不能渲染,不能运行其他代码,就是说被阻塞了。如果你想要一个体验很好,运行流畅的应用,这将会是很大的问题。

而且还不止这一个问题。一旦你的浏览器在调用栈中处理很多任务,它将会在很长时间内得不到响应,大多数浏览器将会抛出一个错误来采取行动,询问你是否要结束这个web页面。

这不是最好的用户体验,不是吗?

所以,我们怎样才能在运行很重的代码的时候,不阻塞UI,使浏览器不需要等待响应呢?解决方案就是异步回调。

我们将会在下一节详细讲述。

对V8引擎的内部机制感兴趣的同学可以看这里

本文翻译自:blog.sessionstack.com/how-does-ja…

关注下面的标签,发现更多相似文章
评论