www.129028.com金沙:JS 引擎底层的工作原理

日期:2020-05-07编辑作者:Web前端

时间: 2019-09-09阅读: 134标签: 原理

时间: 2019-05-17阅读: 280标签: 引擎

有没有想过浏览器如何读取和运行JS代码? 这看起来很神奇,我们可以通过浏览器提供的控制台来了解背后的一些原理。

你有没有想过浏览器是如何读取和运行 JavaScript 代码的吗?这看起来很神奇,但你可以学到一些发生在幕后的事情。让我们通过介绍 JavaScript 引擎的精彩世界在这种语言中尽情畅游。

在Chrome中打开浏览器控制台,然后查看Sources这栏,在右侧可以到一个Call Stack盒子。

在 Chrome 中打开浏览器控制台,然后查看“Sources”标签。你会看到一个有趣的命名:Call Stack(在Firefox中,你可以在代码中插入一个断点后看到调用栈):

JS 引擎是一个可以编译和解释我们的JS代码强大的组件。 最受欢迎的JS 引擎是V8,由 Google Chrome 和 Node.j s使用,SpiderMonkey 用于Firefox,以及Safari/WebKit使用的 JavaScriptCore。

什么是调用栈(Call Stack)?看上去像是有很多东西正在运行,即使是只执行几行代码也是如此。实际上,并不是在所有 Web 浏览器上都能对 JavaScript 做到开箱即用。

虽然现在 JS 引擎不是帮我们处理全面的工作。但是每个引擎中都有一些较小的组件为我们做繁琐的的工作。

有一个很大的组件来编译和解释我们的 JavaScript 代码:它就是 JavaScript 引擎。最受欢迎的 JavaScript 引擎是V8,在 Google Chrome 和 Node.js 中使用,SpiderMonkey 用于 Firefox,以及 Safari/WebKit 所使用的 JavaScriptCore。

其中一个组件是调用堆栈(Call Stack),与全局内存和执行上下文一起运行我们的代码。

今天的 JavaScript 引擎是个很杰出的工程,尽管它不可能覆盖浏览器工作的方方面面,但是每个引擎都有一些较小的部件在为我们努力工作。

Js 引擎和全局内存(Global Memory)

其中一个组件是调用栈,它与全局内存执行上下文一起运行我们的代码。你准备好迎接他们了吗?

JavaScript 是编译语言同时也是解释语言。信不信由你,JS 引擎在执行代码之前只需要几微秒就能编译代码。

JavaScript 引擎和全局内存

这听起来很神奇,对吧?这种神奇的功能称为JIT(及时编译)。这个是一个很大的话题,一本书都不足以描述JIT是如何工作的。但现在,我们午饭可以跳过编译背后的理论,将重点放在执行阶段,尽管如此,这仍然很有趣。

我认为 JavaScript 既是编译型语言又是解释型语言。信不信由你,JavaScript 引擎在执行之前实际上编译了你的代码。

考虑以下代码:

是不是听起来很神奇?这种魔术被称为 JIT(即时编译)。它本身就是一个很大的话题,即使是一本书也不足以描述 JIT 的工作原理。但是现在我们可以跳过编译背后的理论,专注于执行阶段,这仍然是很有趣的。

var num = 2;function pow(num) { return num * num;}

先看以下代码:

如果问你如何在浏览器中处理上述代码?你会说些什么? 你可能会说“浏览器读取代码”或“浏览器执行代码”。

var num = 2;function pow(num) { return num * num;}

现实比这更微妙。首先,读取这段代码的不是浏览器,是JS引擎。JS引擎读取代码,一旦遇到第一行,就会将几个引用放入全局内存

如果我问你如何在浏览器中处理上述代码?你会说些什么?你可能会说“浏览器读取代码”或“浏览器执行代码”。

全局内存(也称为堆)JS引擎保存变量和函数声明的地方。因此,回到上面示例,当 JS引擎读取上面的代码时,全局内存中放入了两个绑定。

现实中比那更加微妙。首先不是浏览器而是引擎读取该代码片段。JavaScript引擎读取代码,当遇到第一行时,就会将一些引用放入全局内存中。

即使示例只有变量和函数,也要考虑你的JS代码在更大的环境中运行:在浏览器中或在Node.js中。 在这些环境中,有许多预定义的函数和变量,称为全局变量。 全球记忆将比num和pow更多。

全局内存(也称为堆)是 JavaScript 引擎用来保存变量和函数声明的区域。所以回到前面的例子,当引擎读取上面的代码时,全局内存中被填充了两个绑定:

上例中,没有执行任何操作,但是如果我们像这样运行函数会怎么样呢:

即使例子中只有变量和函数,也要考虑你的 JavaScript 代码在更大的环境中运行:浏览器或在 Node.js 中。在这些环境中,有许多预定义的函数和变量,被称为全局。全局内存将比 num 和 pow 所占用的空间更多。记住这一点。

var num = 2;function pow(num) { return num * num;}pow(num);

此时没有执行任何操作,但是如果尝试像这样运行我们的函数会怎么样:

现在事情变得有趣了。当函数被调用时,JavaScript引擎会为全局执行上下文调用栈腾出空间。

var num = 2;function pow(num) { return num * num;}pow(num);

JS引擎:它们是如何工作的? 全局执行上下文和调用堆栈

将会发生什么?现在事情变得有趣了。当一个函数被调用时,JavaScript 引擎会为另外两个盒子腾出空间:

刚刚了解了 JS引擎如何读取变量和函数声明,它们最终被放入了全局内存(堆)中。

全局执行上下文环境调用栈全局执行上下文和调用栈

但现在我们执行了一个JS函数,JS引擎必须处理它。怎么做?每个JS引擎中都有一个基本组件,叫调用堆栈

在上一节你了解了 JavaScript 引擎是如何读取变量和函数声明的,他们最终进入了全局内存(堆)。

调用堆栈是一个堆栈数据结构:这意味着元素可以从顶部进入,但如果它们上面有一些元素,它们就不能离开,JS 函数就是这样的。

但是现在我们执行了一个 JavaScript 函数,引擎必须要处理它。怎么处理?每个 JavaScript 引擎都有一个基本组件,称为调用栈

一旦执行,如果其他函数仍然被阻塞,它们就不能离开调用堆栈。请注意,这个有助于你理解“JavaScript是单线程的”这句话。

调用栈是一个栈数据结构:这意味着元素可以从顶部进入,但如果在它们上面还有一些元素,就不能离开栈。 JavaScript 函数就是这样的。

回到我们的例子,当函数被调用时,JS引擎将该函数推入调用堆栈

当函数开始执行时,如果被某些其他函数卡住,那么它无法离开调用堆栈。请注意,因为这个概念有助于理解“JavaScript是单线程”这句话

同时,JS 引擎还分配了一个全局执行上下文,这是运行JS代码的全局环境,如下所示

但是现在让我们回到上面的例子。当调用该函数时,引擎会将该函数压入调用堆栈中:

想象全局执行上下文是一个海洋,其中全局函数像鱼一样游动,多美好! 但现实远非那么简单, 如果我函数有一些嵌套变量或一个或多个内部函数怎么办?

我喜欢将调用栈看作是一叠薯片。如果还没有先吃掉顶部的所有薯片,就吃不到到底部的薯片!幸运的是我们的函数是同步的:它是一个简单的乘法,可以很快的得到计算结果。

即使是像下面这样的简单变化,JS引擎也会创建一个本地执行上下文:

同时,引擎还分配了全局执行上下文,这是 JavaScript 代码运行的全局环境。这是它的样子:

var num = 2;function pow(num) { var fixed = 89; return num * num;}pow(num);

想象一下全局执行环境作为一个海洋,其中 JavaScript 全局函数就像鱼一样在里面游泳。多么美好!但这只是故事的一半。如果函数有一些嵌套变量或一个或多个内部函数怎么办?

注意,我在pow函数中添加了一个名为fixed的变量。在这种情况下,pow函数中会创建一个本地执行上下文,fixed变量被放入pow函数中的本地执行上下文中。

即使在下面的简单变体中,JavaScript 引擎也会创建本地执行上下文

对于嵌套函数的每个嵌套函数,引擎都会创建更多的本地执行上下文。

var num = 2;function pow(num) { var fixed = 89; return num * num;}pow(num);

JavaScript 是单线程和其他有趣的故事

请注意,我在函数 pow 中添加了一个名为 fixed 的变量。在这种情况下,本地执行上下文中将包含一个用于保持固定的框。我不太擅长在小方框里画更小的框!你现在必须运用自己的想象力。

JavaScript是单线程的,因为只有一个调用堆栈处理我们的函数。也就是说,如果有其他函数等待执行,函数就不能离开调用堆栈。

本地执行上下文将出现在 pow 附近,包含在全局执行上下文中的绿色框内。你还可以想象,对于嵌套函数中的每个嵌套函数,引擎都会创建更多的本地执行上下文。这些框可以很快的到达它们该去的地方。

在处理同步代码时,这不是问题。例如,两个数字之间的和是同步的,以微秒为单位。但如果涉及异步的时候,怎么办呢?

单线程的JavaScript

幸运的是,默认情况下JS引擎是异步的。即使它一次执行一个函数,也有一种方法可以让外部(如:浏览器)执行速度较慢的函数,稍后探讨这个主题。

我们说JavaScript 是单线程的,因为有一个调用栈处理我们的函数。也就是说,如果有其他函数等待执行,函数是不能离开调用栈的。

当浏览器加载某些JS代码时,JS引擎会逐行读取并执行以下步骤:

当处理同步代码时,这不是什么问题。例如,计算两个数字的和就是同步的,并且以微秒做为运行单位。但是当进行网络通信和与外界的互动时呢?

将变量和函数的声明放入全局内存(堆)中将函数的调用放入调用堆栈创建全局执行上下文,在其中执行全局函数创建多个本地执行上下文(如果有内部变量或嵌套函数)

幸运的是JavaScript引擎被默认设计为异步。即使他们一次可以执行一个函数,也有一种方法可以让外部实体执行较慢的函数:在我们的例子中是浏览器。我们稍后会探讨这个话题。

到目前为止,对JS引擎的同步机制有了基本的了解。 在接下来的部分中,讲讲 JS 异步工作原理。

这时,你应该了解到当浏览器加载某些 JavaScript 代码时,引擎会逐行读取并执行以下步骤:

异步JS,回调队列和事件循环

使用变量和函数声明填充全局内存(堆)将每个函数调用送到调用栈创建一个全局执行上下文,其在中执行全局函数创建了许多微小的本地执行上下文(如果有内部变量或嵌套函数)

全局内存(堆),执行上下文和调用堆栈解释了同步 JS 代码在浏览器中的运行方式。 然而,我们遗漏了一些东西,当有一些异步函数运行时会发生什么?

到此为止,你脑子里应该有了一个JavaScript 引擎同步机制的全景图www.129028.com金沙,。在接下来的部分中,你将看到异步代码如何在 JavaScript 中工作以及为什么这样工作。

请记住,调用堆栈一次可以执行一个函数,甚至一个阻塞函数也可以直接冻结浏览器。 幸运的是JavaScript引擎是聪明的,并且在浏览器的帮助下可以解决问题。

异步JavaScript,回调队列和事件循环

当我们运行一个异步函数时,浏览器接受该函数并运行它。考虑如下代码:

全局内存、执行上下文和调用栈解释了同步 JavaScript 代码在浏览器中的运行方式。然而我们还错过了一些东西。当有异步函数运行时会发生什么?

setTimeout(callback, 10000);function callback(){ console.log('hello timer!');}

我所指的异步函数是每次与外界的互动都需要一些时间才能完成的函数。例如调用REST API或调用计时器是异步的,因为它们可能需要几秒钟才能运行完毕。 现在的 JavaScript 引擎都有办法处理这种函数而不会阻塞调用堆栈,浏览器也是如此。

setTimeout大家都知道得用得很多次了,但你可能不知道它不是内置的JS函数。 也就是说,当JS 出现,语言中没有内置的setTimeout。

请记住,调用堆栈一次只可以执行一个函数,甚至一个阻塞函数都可以直接冻结浏览器。幸运的是,JavaScript 引擎非常智能,并且能在浏览器的帮助下解决问题。

setTimeout浏览器API( Browser API)的一部分,它是浏览器免费提供给我们的一组方便的工具。这在实战中意味着什么?由于setTimeout是一个浏览器的一个Api,函数由浏览器直接运行(它会在调用堆栈中出现一会儿,但会立即删除)。

当我们运行异步函数时,浏览器会接受该函数并运行它。考虑下面的计时器:

10秒后,浏览器接受我们传入的回调函数并将其移动到回调队列(Callback Queu)中。。考虑以下代码

setTimeout(callback, 10000);function callback(){ console.log('hello timer!');}
var num = 2;function pow(num) { return num * num;}pow(num);setTimeout(callback, 10000);function callback(){ console.log('hello timer!');}

你肯定多次见到过setTimeout,但是你可能不知道它不是一个内置的 JavaScript 函数。即当 JavaScript 诞生时,语言中并没有内置的 setTimeout。

示意图如下:

实际上 setTimeout 是所谓的Browser API的一部分,它是浏览器提供给我们的便利工具的集合。多么体贴!这在实践中意味着什么?由于 setTimeout 是一个浏览器 API,该函数由浏览器直接运行(它会暂时出现在调用栈中,但会立即删除)。

如你所见,setTimeout在浏览器上下文中运行。 10秒后,计时器被触发,回调函数准备运行。 但首先它必须通过回调队列(Callback Queue)。 回调队列是一个队列数据结构,回调队列是一个有序的函数队列。

然后 10 秒后浏览器接受我们传入的回调函数并将其移动到回调队列。此时我们的 JavaScript 引擎中还有两个框。请看以下代码:

每个异步函数在被放入调用堆栈之前必须通过回调队列,但这个工作是谁做的呢,那就是事件循环(Event Loop)。

var num = 2;function pow(num) { return num * num;}pow(num);setTimeout(callback, 10000);function callback(){ console.log('hello timer!');}

事件循环只有一个任务:它检查调用堆栈是否为空。如果回调队列中(Callback Queue)有某个函数,并且调用堆栈是空闲的,那么就将其放入调用堆栈中。

可以这样画完成我们的图:

完成后,执行该函数。 以下是用于处理异步和同步代码的JS引擎的图:

如你所见setTimeout 在浏览器上下文中运行。 10秒后,计时器被触发,回调函数准备好运行。但首先它必须通过回调队列。回调队列是一个队列数据结构,顾名思义是一个有序的函数队列。

想象一下,callback()已准备好执行,当pow()完成时,调用堆栈(Call Stack) 为空,事件循环(Event Look) 将callback()放入调用堆中。大概就是这样,如果你理解了上面的插图,那么你就可以理解所有的JavaScript了。

每个异步函数在被送入调用栈之前必须通过回调队列。但谁推动了这个函数呢?还有另一个名为Event Loop的组件。

回调地狱和 ES6 中的Promises

Event Loop 现在只做一件事:它应检查调用栈是否为空。如果回调队列中有一些函数,并且如果调用栈是空闲的,那么这时应将回调送到调用栈。在完成后执行该函数。

JS 中回调函数无处不在,它们用于同步和异步代码。 考虑如下map方法:

这是用于处理异步和同步代码的 JavaScript 引擎的大图

function mapper(element){ return element * 2;}[1, 2, 3, 4, 5].map(mapper);

想象一下,callback() 已准备好执行。当 pow() 完成时,调用栈为空,事件循环推送callback()。就是这样!即使我简化了一些东西,如果你理解了上面的图,那么就可以理解 JavaScript 的一切了。

mapper是一个在map内部传递的回调函数。上面的代码是同步的,考虑异步的情况:

请记住:Browser API、回调队列和事件循环是异步 JavaScript 的支柱

function runMeEvery(){ console.log('Ran!');}setInterval(runMeEvery, 5000);

如果你喜欢视频,我建议去看 Philip Roberts 的视频:事件循环是什么。这是关于时间循环的最好的解释之一。

该代码是异步的,我们在setInterval中传递回调runMeEvery。回调在JS中无处不在,因此就会出现了一个问题:回调地狱

youtube:

JavaScript 中的回调地狱指的是一种编程风格,其中回调嵌套在回调函数中,而回调函数又嵌套在其他回调函数中。由于 JS 异步特性,js 程序员多年来陷入了这个陷阱。

坚持下去,因为我们还没有使用异步 JavaScript。在后面的内容中,我们将详细介绍 ES6 Promises。

说实话,我从来没有遇到过极端的回调金字塔,这可能是因为我重视可读代码,而且我总是坚持这个原则。如果你在遇到了回调地狱的问题,说明你的函数做得太多。

回调地狱和 ES6 的 Promise

这里不会讨论回调地狱,如果你好奇,有一个网站,callbackhell.com,它更详细地探索了这个问题,并提供了一些解决方案。

JavaScript 中的回调函数无处不在。它们用于同步和异步代码。例如 map 方法:

我们现在要关注的是ES6的Promises。ES6 Promises是JS语言的一个补充,旨在解决可怕的回调地狱。但什么是 Promises 呢?

function mapper(element){ return element * 2;}[1, 2, 3, 4, 5].map(mapper);

JS的 Promise是未来事件的表示。 Promise 可以以成功结束:用行话说我们已经解决了resolved(fulfilled)。 但如果 Promise 出错,我们会说它处于拒绝(rejected )状态。 Promise 也有一个默认状态:每个新的 Promise 都以挂起(pending)状态开始。

mapper 是在 map 中传递的回调函数。上面的代码是同步的。但要考虑一个间隔:

创建和使用 JavaScript 的 Promises

function runMeEvery(){ console.log('Ran!');}setInterval(runMeEvery, 5000);

要创建一个新的 Promise,可以通过传递回调函数来调用 Promise 构造函数。回调函数可以接受两个参数:resolve和reject。如下所示:

该代码是异步的,我们在 setInterval 中传递了回调 runMeEvery。回调在 JavaScript 中很普遍,所以近几年里出现了一个问题:回调地狱。

const myPromise = new Promise(function(resolve){ setTimeout(function(){ resolve() }, 5000)});

JavaScript中的回调地狱指的是编程的“风格”,回调嵌套在嵌套在…...其他回调中的回调中。正是由于 JavaScript 的异步性质导致程序员掉进了这个陷阱。

如下所示,resolve是一个函数,调用它是为了使Promise 成功,别外也可以使用reject来表示调用失败。

说实话,我从来没有碰到过极端的回调金字塔,也许是因为我重视代码的可读性,并且总是试着坚持这个原则。如果你发现自己掉进了回调地狱,那就说明你的函数太多了。

const myPromise = new Promise(function(resolve, reject){ setTimeout(function(){ reject() }, 5000)});

我不会在这里讨论回调地狱,如果你很感兴趣的话,给你推荐一个网站:callbackhell.com更深入地探讨了这个问题并提供了一些解决方案。我们现在要关注的是ES6 Promise。 ES6 Promise 是对 JavaScript 语言的补充,旨在解决可怕的回调地狱。但 Promise 是什么?

注意,在第一个示例中可以省略reject,因为它是第二个参数。但是,如果打算使用reject,则不能忽略resolve,如下所示,最终将得到一个resolved的承诺,而非reject。

JavaScript Promise 是未来事件的表示。Promise 能够以 success 结束:用行话说就是它已经resolved(已经完成)。但如果 Promise 出错,我们会说它处于rejected状态。 Promise 也有一个默认状态:每个新Promise都以pending状态开始。

// 不能忽略 resolve !const myPromise = new Promise(function(reject){ setTimeout(function(){ reject() }, 5000)});

创建和使用 Promise

现在,Promises看起来并不那么有用,我们可以向它添加一些数据,如下所示:

要创建新的 Promise,可以通过将回调函数传给要调用的 Promise 构造函数的方法。回调函数可以使用两个参数:resolvereject。让我们创建一个新的 Promise,它将在5秒后 resolve(你可以在浏览器的控制台中尝试这些例子):

本文由www.129028.com金沙发布于Web前端,转载请注明出处:www.129028.com金沙:JS 引擎底层的工作原理

关键词:

nginx负载均衡如何实现www.129028.com金沙?

什么是nginx? Nginx是一个免费的,开源的,高性能的服务器和反向代理服务器软件,同时它也可以为IMAP和POP3服务器代...

详细>>

最少编码原则

这的确是大多数程序员,甚至是那些高级程序员都很容易混淆的一个重点。作为一名程序员,编写代码无疑是你职业...

详细>>

CSS中cursor 鼠标指针光标样式

值 前面url()是自定义鼠标的样式,图像的地址,后面的参数是 css 标准的 cursor样式,(IE下面可以不需要) 出现版本...

详细>>

AJAX:如何处理书签和后退按钮(1)

或者如果你不喜欢onclick: window.onload = initialize;function initialize() { // initialize the DHTML History // framework dhtmlHistory.initial...

详细>>