随着 Javascript
越来越流行,使其应用的场景越来越多,不仅限于前端,可以是后端、混合应用程序、嵌入式设备等等,于是就有了大前端的叫法。本文开始带大家一起回顾总结 Javascript
的构建块以及它们是如何协同工作,理解其原理,将有助于编写更优的代码。
概述
大部份人都听说过 V8 引擎,都知道 Javascript
是单线程的,或者它使用的是回调队列。下面将围绕这些概念展开,并解释 Javascript
的实际运行方式。通过了解这些细节,能够编写更好的、无阻塞的应用程序,并正确利用所提供的 API。
Javascript 引擎
Javascript 引擎中最流行的莫过于 Google 开发的 V8 引擎,可以用于 Chrome 和 Node.js
。下面是它的架构图:
引擎由两个主要部份组成:
- 内存堆:内存分配发生的地方
- 调用栈:代码执行时的堆栈帧所在的地方
Runtime
浏览器中的 API 几乎大部份 Javascript 开发工程师都使用过(例如:setTimeout
)。但是,这些 API 并不是由 Javascript 引擎提供的。那么,它们是谁提供的呢?来自哪里?
因此除了引擎,实际上还有很多,如浏览器提供的 Web Api,如:DOM
、 AJAX
、 setTimeout
等等。
然后,还有非常流行的 event loop
和 callback queue
。
调用栈
Javascript 是一种单线程编程语言,这意味着它只有一个调用堆栈,因此它一次只能做一件事。
调用栈是一种数据结构,它基本上记录了在程序中的位置。如果进入一个函数,会将它放在栈顶。如果从一个函数返回,会从栈顶推出。这是堆栈可以做的所有事情。
看看下面的代码:
const multiply = (x, y) => { return x * y; }; const square = (x) => { const s = multiply(x, x); console.log(s); }; square(6);
当引擎开始执行此代码时,调用堆栈开始为空,之后,步骤如下:
调用堆栈中的每个条目称为堆栈帧。
这正是抛出异常时堆栈跟踪的构造方式,它基本上是异常发生时调用堆栈的状态。在来看看下面的代码:
function foo() { throw new Error("foo异常抛出!"); } function bar() { foo(); } function start() { bar(); } start();
如果在 Chrome 中执行此操作(假设此代码位于名为 foo.js
的文件中),将生成以下堆栈跟踪:
Blowing the stack
:当达到最大调用堆栈大小时会发生这种情况,这种情况很容易发生,特别是如果使用递归而没有全面测试。看看这个示例代码:
function foo() { foo(); } foo();
当引擎开始执行这段代码时,它首先调用函数 foo
。然而,这个函数是递归调用的,并且在没有任何终止条件的情况下开始调用自身。因此,在执行的每一步,相同的函数都会一遍又一遍地添加到调用堆栈中。它看起来像这样:
然而,在某些时候,调用堆栈中的函数调用数量超过了调用堆栈的实际大小,浏览器决定采取行动,抛出一个错误,它可能如下所示:
在单线程上运行代码非常容易出现这种情况。
并发和事件循环
如果调用堆栈中的函数调用需要花费大量时间来处理,会发生什么情况?例如,假设想在浏览器中使用 Javascript 进行一些复杂的图像转换。
如果调用堆栈有要执行的函数,浏览器实际上不能做任何其他事情,这个时候处于阻塞状态。这意味着浏览器无法渲染,无法运行任何其他代码,出现卡住的现象。如果想在应用中使用流畅的 UI,就需要避免阻塞情况的出现。
如果浏览器开始在调用堆栈中处理很多的任务,它可能会在很长一段时间内停止响应,大多数浏览器通过触发错误来终止此类行动,并询问是否要终止网页。
总结
本文简单介绍了 Javascript 的引擎、运行时和调用堆栈,调用堆栈阻塞、卡死的情况将在接下来的《深入了解V8引擎》介绍优化技巧。