[译] JavaScript 如何工做:对引擎、运行时、调用堆栈的概述

原文地址: https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cfjavascript

PS: 很久没写东西了,最近一直在准备写一个本身的博客,最后一些技术方向已经敲定了,又能够开心的学习了,node系列后续再开始。前端


  随着JavaScript愈来愈流行,愈来愈多的团队普遍的把JavaScript应用到前端、后台、hybrid 应用、嵌入式等等领域。java

  这篇文章旨在深刻挖掘JavaScript,以及向你们解释JavaScript是如何工做的。咱们经过了解它的底层构建以及它是怎么发挥做用的,能够帮助咱们写出更好的代码与应用。据 GitHut 统计显示,JavaScript 长期占据GitHub中 Active RepositoriesTotal Pushes 的榜首,而且在其余的类别中也不会落后太多。node

  若是一个项目愈来愈依赖 JavaScript,这就意味着开发人员必须利用这些语言和生态系统提供更深层次的核心内容去构建一个使人振奋的应用。然而,事实证实,有不少的开发者天天都在使用 JavaScript,可是殊不知道在底层 JavaScript 是怎么运做的。web

概述

  几乎每一个人据说过 V8 引擎的概念,并且,大多数人都知道 JavaScript 是单线程的,或者是它是使用回调队列的。浏览器

  在这篇文章中,咱们将详细的介绍这些概念,并解释 JavaScript 是怎么工做的。经过了解这些细节,你就能利用这些提供的 API 来写出更好的,非阻塞的应用来。若是你对 JavaScript 比较陌生,那么这篇文章将帮助您理解为何 JavaScript 相较于其余语言显得如此“怪异”。若是您是一位经验丰富的 JavaScript 开发人员,但愿它能给你带来一些新的看法,说明 JavaScript 的运行时,尽管你可能天天都会用到它。session

JavaScript 引擎

  JavaScript 引擎提及来最流行的固然是谷歌的 V8 引擎了, V8 引擎使用在 Chrome 以及 Node 中,下面有个简单的图能说明他们的关系:数据结构

  这个引擎主要由两部分组成:多线程

  • 内存堆:这是内存分配发生的地方
  • 调用栈:这是你的代码执行时的地方

运行时

  有些浏览器的 API 常常被使用到(好比说:setTimeout),可是,这些 API 却不是引擎提供的。那么,他们是从哪儿来的呢?事实上这里面实际状况有点复杂。并发

  因此说咱们还有不少引擎以外的 API,咱们把这些称为浏览器提供的 Web API,好比说 DOM、AJAX、setTimeout等等。

  而后咱们还拥有如此流行的事件循环和回调队列。

调用栈

  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 函数开始。而后这是个递归的函数,而且在没有任何的终止条件的状况下开始调用本身。所以,每执行一步,就会把这个相同的函数一次又一次地添加到调用堆栈中。而后它看起来就像是这样的:

  而后,在某一时刻,调用栈中的函数调用的数量超过了调用栈的实际大小,浏览器决定干掉它,抛出一个错误,它看起来就像是这样:

  在单个线程上运行代码很容易,由于你没必要处理在多线程环境中出现的复杂场景——例如死锁。可是在一个线程上运行也很是有限制。因为 JavaScript 只有一个调用堆栈,当某段代码运行变慢时会发生什么?

并发与事件循环

  调用栈中的函数调用须要大量的时间来处理,那么这会发生什么状况呢?例如,假设你想在浏览器中使用 JavaScript 进行一些复杂的图片转码。

  你可能会问?这算什么问题?事实上,问题是当调用栈有函数要执行,浏览器就不能作任何事,它会被堵塞住。这意味着浏览器不能渲染,不能运行其余的代码,它被卡住了。若是你想在应用里让 UI 很流畅的话,这就会产生问题。

  并且这不是惟一的问题,一旦你的浏览器开始处理调用栈中的众多任务,它可能会中止响应至关长一段时间。大多数浏览器都会这么作,报一个错误,询问你是否想终止 web 页面。

  这样看来,这并非最好的用户体验,不是吗?

  那么,如何在不阻塞 UI 的状况下执行复杂的代码,让浏览器不会不响应?解决方案就是异步回调。这将在“ JavaScript 如何工做”教程的第2部分中详细解释:“在V8引擎中,如何编写优化代码”。