【译】JavaScript 运行原理(一):引擎,运行时,调用栈的概述

627 阅读6分钟

原文连接 How JavaScript works: an overview of the engine, the runtime, and the call stack by Alexander Zlatkov

这个JavaScript 运行原理的第一章,一共18章,会后续持续更新,尽情期待,也可以关注一下,防止迷路


由于JavaScript越来越流行,各个团队都在许多不同的技术栈中使用它—前端,后端,混合APPs,嵌入式设备等等。

这篇文章是整个系列中的第一篇,旨在深入探讨Javascript和它的运行原理:我们认为,通过了解JavaScript的组成部分以及它们如何一起发挥作用,你能够编写出更好的代码和应用。我们还将分享在构建SessionStack(作者所在的公司)时使用的一些经验法则,SessionStack是一种轻量级JavaScript应用程序,必须强大且高性能才能保持竞争力。

如同GitHut stats展示的一样,就活动资源仓库和GitHub中的Push总数而言,JavaScript排名第一。在其他类别中也没有落后太多。

如果项目越来越依赖JavaScript,则意味着开发人员必须利用语言和生态系统提供的所有内容,并对内部进行越来越深入的了解,才能构建出色的软件。

但是事实证明,有很多开发人员每天都在使用JavaScript,但并不了解其内部原理。

概述

几乎每个人都已经听说过V8引擎,而且大多数人都知道JavaScript是单线程的,或者它正在使用回调队列。

在本文中,我们将详细介绍所有这些概念,并解释JavaScript实际如何运行。通过了解这些详细信息,能够编写更好地,无阻塞的应用程序,这些应用程序可以正确利用提供的API。

如果你是JavaScript的新手,此博客文章将帮助您了解JavaScript与其他语言相比为何如此“古怪”。

如果你是一位经验丰富的JavaScript开发人员,希望它会为你提供你每天使用的JavaScript运行时实际工作方式的新见解。

一. JavaScript 引擎

一个流行的JavaScript引擎是Google的V8,V8被用于chrome和Node.js中。举个例子,下图是V8的一个简单视图:

引擎包含两个主要的组件:

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

二. 运行时

浏览器中有许多API几乎所有的JavaScript开发人员都会用到(例如:"setTimeout")。这些API都不是JavaScript引擎提供的。

因此,他们从哪里来的?

事实证明,现实情况有点复杂。

因此,虽然我们有引擎,但是还有很多其他未知的内容。我们称之为Web API, 由浏览器提供,例如DOM,AJAX,setTimeout 等等。

当然,我们还有非常流行的事件循环(event loop)和回调队列(callback queue)。

三. 调用栈

Javascript 是单线程的编程语言,也就意味着它只有一个调用栈。因为在同一时间我们只能做同一件事情。

调用栈是一种数据结构,它记录了我们在程序中的位置。如何进入了一个函数,我们就将这个函数从顶部放入栈中,如果从函数返回,我们就将该函数从栈顶移出栈。这就是调用栈所做的事情。

举个例子,看以下代码

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();

当引擎开始执行这段代码,一开始调用了函数“foo”。但是这个函数是递归的,它开始调用自己而且没有终止条件。因此每一步的执行都是这个函数一遍一遍的往栈里面添加自己。它像下面这样:

在某个时间点,调用栈中的函数执行达到了调用栈中的最大值,浏览器决定通过抛出异常,像下文一样:
在单线程模式运行代码非常的容易,因为你不用去处理那些在多线程环境下的复杂情况,比如死锁。

单线程模式也同样有一些限制,Javascript 只有一个调用栈,当调用栈上的当前代码执行的时间很长怎么办?

并发 & 事件循环(Concurrency & 事件循环)

当你的函数在调用栈中为了处理某个任务消耗了大量的时间怎么办?想象一下,你需要在浏览器中通过JavaScript转换某些复杂的图片。

你也许会问,为什么这是一个问题?这个问题关键在于当调用栈在执行的时候,浏览器不能执行其的任务—它被阻塞了。这也意味着浏览器不能渲染,不能运行其他任务代码,它就被卡在那。如果你希望的是一个流程的web应用,这将是一个问题。

这还不是唯一的问题。一旦浏览器开始处理“调用堆栈”中的许多任务,它可能会在相当长的时间内停止响应。而且大多数浏览器采取的方案都是抛出错误,询问您是否要终止网页。

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

因此我们如何执行耗时的代码而不去阻塞UI和导致浏览器不响应?方案是异步回调。

这个会在“JavaScript 运行原理”系列的第二章“深入了解V8引擎 & 如何写出最优代码的5个提示”中进行讲解。

本系列其他文章

  1. 关于引擎,运行时,调用栈的概述
  2. 深入了解V8引擎 & 如何写出最优代码的5个提示
  3. 内存管理 & 如何处理4种常见的内存泄露
  4. 事件循环机制和异步编程的兴起 & 通过async/await更好的编码的5种方法
  5. 通过SSE深入了解WebSockets和HTTP2 & 如何选择正确的路径
  6. 对比WebAssembly & 为什么某种情况下它要优于JavaScript
  7. Web Workers的构 & 你需要用都它的5种情况
  8. Service Workers,它的生命周期和使用案例
  9. Web Push Notifications的机制
  10. 通过MutatioinObserver跟踪DOM的变化
  11. 渲染引擎和优化技巧
  12. 深入了解网络层 & 性能优化和安全性
  13. 理解CSS和JS动画的内部原理 & 性能优化
  14. 解析,抽象语法树(ASTs) & 如何优化解析时间
  15. 类和继承的内部原理 & Babel和TypeScript转义(transpiling)
  16. Storage 引擎 & 如何选择合适的存储API
  17. Shadow Dom 的内部原理 & 如何构建独立的组件
  18. WebRTC和对等网络机制