JavaScript引擎:它们如何工作? 从调用堆栈到承诺,(几乎)您需要了解的所有内容


您是否想知道浏览器如何读取和执行JavaScript代码? 它看起来很神秘,但是在这篇文章中,您可以了解幕后发生的事情。

我们从游览JavaScript引擎的精彩世界开始我们的语言之旅。

在Chrome中打开控制台,然后转到“来源”标签。 您将看到几个部分,其中最有趣的部分是调用堆栈 (在Firefox中,将断点放入代码中将看到调用堆栈):



什么是调用栈? 即使为了执行几行代码,这里似乎也发生了很多事情。 实际上,并不是所有浏览器都附带JavaScript。 有一个很大的组件可以编译和解释我们的JavaScript代码-这是一个JavaScript引擎。 最受欢迎的是V8,它用于Google Chrome和Node.js,Firefox中的SpiderMonkey,Safari / WebKit中的JavaScriptCore。

今天的JavaScript引擎是软件工程的典范,几乎不可能谈论所有方面。 但是,代码执行的主要工作仅由引擎的几个组件完成:调用堆栈(调用堆栈),全局内存(全局内存)和执行上下文(执行上下文)。 准备见他们吗?

内容:

  1. JavaScript引擎和全局内存
  2. JavaScript引擎:它们如何工作? 全局执行上下文和调用堆栈
  3. JavaScript是单线程的,还有其他有趣的故事
  4. 异步JavaScript,回调队列和事件循环
  5. 回调地狱并承诺ES6
  6. 创建和使用JavaScript Promises
  7. ES6承诺中的错误处理
  8. ES6 Promise组合器:Promise.all,Promise.allSettled,Promise.any和其他
  9. ES6承诺和微任务队列
  10. JavaScript引擎:它们如何工作? 异步演进:从承诺到异步/等待
  11. JavaScript引擎:它们如何工作? 总结

1. JavaScript引擎和全局内存


我说过,JavaScript既是编译语言,又是解释语言。 信不信由你,JavaScript引擎实际上在执行代码之前就会编译您的代码。

是某种魔术,对吧? 这种魔术称为JIT(及时编译)。 仅这是一个很大的讨论主题,甚至书籍也不足以描述JIT的工作。 但是现在,我们将跳过理论,而将重点放在执行阶段,这同样很有趣。

首先,请看以下代码:

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

假设我问您如何在浏览器中处理此代码? 你会怎么回答? 您可以说:“浏览器读取代码”或“浏览器执行代码”。 实际上,一切都不是那么简单。 首先,代码不是由浏览器读取,而是由引擎读取。 JavaScript引擎读取代码 ,并在定义第一行后立即将几个链接放入全局内存

全局内存(也称为堆)是JavaScript引擎在其中存储变量和函数声明的区域。 当他阅读上面的代码时,两个绑定器将出现在全局存储器中:



即使示例仅包含变量和函数,也可以想象您的JavaScript代码是在更大的环境中执行的:在浏览器或Node.js中。 在这样的环境中,有许多预定义的函数和变量称为全局变量。 因此,请记住,全局内存将不仅包含numpow ,还包含更多的数据。

目前没有任何反应。 现在让我们尝试执行我们的功能:

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

会发生什么? 将会发生一些有趣的事情。 调用该函数时,JavaScript引擎将突出显示两个部分:

  • 全局执行上下文
  • 调用堆栈

什么啊

2. JavaScript引擎:它们如何工作? 全局执行上下文和调用堆栈


您了解了JavaScript引擎如何读取变量和函数声明。 它们落入全局内存(堆)中。

但是现在我们正在执行JavaScript函数,引擎应该注意这一点。 怎么了 每个JavaScript引擎都有一个称为调用栈关键组件

这是一个堆叠的数据结构 :可以从上方将元素添加到其中,但是当它们上方还有其他元素时,不能将它们从结构中排除。 这就是JavaScript函数的工作方式。 在执行时,如果其中包含另一个函数,则它们无法离开调用堆栈。 请注意这一点,因为该概念有助于理解“ JavaScript是单线程的”语句。

但是回到我们的例子。 调用函数时,引擎会将其发送到调用堆栈



我喜欢将呼叫堆栈显示为Pringles芯片堆栈。 在吃掉顶部的薯条之前,我们不能从堆栈的底部开始吃薯条。 幸运的是,我们的函数是同步的:它只是一个快速计算出的乘法。

同时,引擎将全局执行上下文放在内存中,这是在其中执行JavaScript代码的全局环境。 看起来是这样的:



想象一下海洋中的全局执行上下文,其中全局JavaScript函数像鱼一样漂浮。 真甜! 但这只是故事的一半。 如果我们的函数具有嵌套变量或内部函数怎么办?

即使在简单的情况下,如下所示,JavaScript引擎也会创建本地执行上下文

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

请注意,我将fixed变量添加到pow函数。 在这种情况下,本地执行上下文将包含fixed 。 我不太擅长在其他小矩形内绘制小矩形,因此请发挥您的想象力。

本地执行上下文将出现在pow旁边,位于全局执行上下文内的绿色矩形部分内。 还要想象一下,引擎如何为嵌套函数内的每个嵌套函数创建其他本地执行上下文。 所有这些矩形部分很快就会出现! 像一个嵌套娃娃!

让我们回到单线程的故事。 这是什么意思?

3. JavaScript是单线程的,还有其他有趣的故事


我们说JavaScript是单线程的,因为只有一个调用堆栈可以处理我们的函数 。 让我提醒您,如果其他函数需要执行,则函数不能离开调用堆栈。

如果我们使用同步代码,这不是问题。 例如,两个数字的加法是同步的,并且以微秒为单位进行计算。 网络通话以及与外界的其他交互又如何呢?

幸运的是, JavaScript引擎默认情况下被设计为异步工作 。 即使它们一次只能执行一个功能,也可以由外部实体执行较慢的功能-在我们的例子中,它是浏览器。 我们将在下面讨论。

同时,您知道当浏览器加载某种JavaScript代码时,引擎会逐行读取此代码并执行以下步骤:

  • 将变量和函数声明放入全局内存(堆)。
  • 将调用发送到调用堆栈上的每个函数。
  • 创建在其中执行全局功能的全局执行上下文。
  • 创建许多小的本地执行上下文(如果有内部变量或嵌套函数)。

现在,您已经对所有JavaScript引擎基础的同步机制有了基本的了解。 在下一章中,我们将讨论异步代码如何在JavaScript中工作以及为什么这样工作。

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


多亏了全局内存,执行上下文和调用堆栈,同步的JavaScript代码才能在我们的浏览器中执行。 但是我们忘记了一些。 如果您需要执行某种异步功能会怎样?

异步功能是指与外界的每一次互动,这可能需要一些时间才能完成。 调用REST API或计时器是异步的,因为执行它们可能需要几秒钟。 借助引擎中可用的元素,我们可以处理这些功能而不会阻塞调用堆栈和浏览器。 别忘了,调用堆栈一次只能执行一个功能, 甚至一个阻塞功能也可以从字面上使浏览器停止 。 幸运的是,JavaScript引擎很聪明,并且在浏览器的帮助下,它们可以解决问题。

当我们执行异步功能时,浏览器会使用它并为我们执行它。 像这样一个计时器:

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

我敢肯定,尽管您已经看过数百次setTimeout ,但是您可能不知道JavaScript中没有内置此功能 。 因此,当JavaScript出现时,其中没有setTimeout函数。 实际上,它是所谓的浏览器API的一部分,浏览器API是浏览器为我们提供的便捷工具的集合。 太好了! 但是,这实际上意味着什么? 由于setTimeout属于浏览器API,因此该功能由浏览器本身执行(暂时出现在调用堆栈中,但立即从那里删除)。

10秒钟后,浏览器将采用我们传递给它的回调函数,并将其放入回调队列中 。 目前,JavaScript引擎中还出现了两个矩形区域。 看一下这段代码:

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

现在我们的方案如下所示:



setTimeout在浏览器上下文中执行。 10秒钟后,计时器启动,回调函数准备执行。 但是首先,它必须经过回调队列。 这是队列形式的数据结构,顾名思义,它是函数的有序队列。

每个异步函数必须先通过回调队列,然后才能进入调用堆栈。 但是谁下一步发送功能呢? 这使一个称为事件循环的组件成为可能

到目前为止,事件循环仅处理一件事:它检查调用堆栈是否为空。 如果回调队列中有任何函数,并且调用堆栈是空闲的,那么该将回调发送到调用堆栈的时候了。

之后,该功能被视为已执行。 这是使用JavaScript引擎处理异步和同步代码的通用方案:



假设callback()已准备好执行。 当pow() 调用栈被释放,事件循环 callback() 发送 callback() 。 就是这样! 尽管我做了一些简化,但是如果您理解了上图,就可以理解所有JavaScript。

请记住: 基于浏览器的API,回调队列和事件循环是异步JavaScript的基础

而且,如果您有兴趣,可以观看Philip Roberts的好奇视频“无论如何,事件循环到底是怎么回事”。 这是事件循环的最佳解释之一。

但是我们还没有完成异步JavaScript主题。 在以下各章中,我们将考虑ES6承诺。

5.回调地狱和ES6承诺


回调函数在所有地方的JavaScript中都可以使用,包括同步代码和异步代码。 考虑以下方法:

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

mapper是在map内部传递的回调函数。 上面的代码是同步的。 现在考虑这个间隔:

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

这段代码是异步的,因为在setInterval内部我们传递了runMeEvery回调。 回调在整个JavaScript中都使用,因此多年来,我们一直遇到一个称为“回调地狱”的问题。

JavaScript中的“ 回调地狱 ”一词适用于编程的“样式”,在该样式中,回调函数嵌入在其他回调函数中,而其他回调函数又嵌入在其他回调函数中...由于异步特性,JavaScript程序员早就陷入了这一陷阱。

老实说,我从未创建过大型的回调金字塔。 也许是因为我重视可读代码,并始终尝试坚持其原则。 如果您遇到了回调难题,则意味着您的函数执行了太多操作。

如果您有兴趣,我不会详细讨论回调地狱,然后转到callbackhell.com ,在该处已详细研究了此问题,并提出了各种解决方案。 我们将讨论ES6承诺 。 这是一个JavaScript插件,旨在解决地狱回调问题。 但是什么是诺言?

JavaScript承诺表示未来事件 。 一个承诺可能会成功结束,或者在程序员的行话中,一个承诺将被“解决”(解决)。 但是,如果承诺以错误结尾,那么我们说它处于拒绝状态。 承诺也具有默认状态:每个新的承诺都以挂起状态开始。 我可以创造自己的诺言吗? 是的 我们将在下一章中讨论这一点。

6.创建和使用JavaScript promises


要创建新的Promise,您需要通过向其传递回调函数来调用构造函数。 它只能采用两个参数: resolvereject 。 让我们创建一个新的Promise,它将在5秒内解决(您可以在浏览器控制台中测试示例):

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

如您所见, resolve是我们调用的一个函数,以使诺言成功结束。 reject会创建一个被拒绝的承诺:

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

请注意,您可以忽略reject因为这是第二个参数。 但是,如果您打算使用reject则不能忽略resolve 。 也就是说,以下代码将不起作用,并以允许的承诺结尾:

 // Can't omit resolve ! const myPromise = new Promise(function(reject){ setTimeout(function(){ reject() }, 5000) }); 

承诺现在看起来不太有用,对吧? 这些示例不向用户显示任何内容。 让我们添加一些东西。 允许的,被拒绝的承诺可以返回数据。 例如:

 const myPromise = new Promise(function(resolve) { resolve([{ name: "Chris" }]); }); 

但是我们仍然看不到任何东西。 要从诺言中提取数据,您需要将诺言与then方法关联 。 他进行了回调(具有讽刺意味!),该回调接收当前数据:

 const myPromise = new Promise(function(resolve, reject) { resolve([{ name: "Chris" }]); }); myPromise.then(function(data) { console.log(data); }); 

作为JavaScript开发人员和他人代码的使用者,您通常会与外部承诺进行交互。 库创建者通常将旧代码包装在Promise构造函数中,如下所示:

 const shinyNewUtil = new Promise(function(resolve, reject) { // do stuff and resolve // or reject }); 

并且,如有必要,我们还可以通过调用Promise.resolve()来创建和解决承诺:

 Promise.resolve({ msg: 'Resolve!'}) .then(msg => console.log(msg)); 

因此,让我提醒您:JavaScript承诺是将来发生的事件的书签。 事件以“等待决策”状态开始,可以成功(允许,执行),也可以不成功(被拒绝)。 许诺可以返回可以通过附加then检索到的数据。 在下一章中,我们将讨论如何处理来自承诺的错误。

7. ES6承诺中的错误处理


在JavaScript中处理错误总是很容易的,至少在同步代码中是如此。 看一个例子:

 function makeAnError() { throw Error("Sorry mate!"); } try { makeAnError(); } catch (error) { console.log("Catching the error! " + error); } 

结果将是:

 Catching the error! Error: Sorry mate! 

正如预期的那样,错误落入catch 。 现在尝试异步函数:

 function makeAnError() { throw Error("Sorry mate!"); } try { setTimeout(makeAnError, 5000); } catch (error) { console.log("Catching the error! " + error); } 

由于setTimeout此代码是异步的。 如果执行它会发生什么?

  throw Error("Sorry mate!"); ^ Error: Sorry mate! at Timeout.makeAnError [as _onTimeout] (/home/valentino/Code/piccolo-javascript/async.js:2:9) 

现在结果不同了。 错误没有被catch ,而是自由地上升到堆栈上。 原因是try/catch仅适用于同步代码。 如果您想了解更多, 这里将详细讨论这个问题。

幸运的是,有了承诺,我们就可以像处理异步错误一样处理它们。 在上一章中,我说过调用reject会导致对诺言的拒绝:

 const myPromise = new Promise(function(resolve, reject) { reject('Errored, sorry!'); }); 

在这种情况下,我们可以通过再次使用回调来使用catch处理程序来处理错误:

 const myPromise = new Promise(function(resolve, reject) { reject('Errored, sorry!'); }); myPromise.catch(err => console.log(err)); 

另外,要在正确的位置创建和拒绝承诺,可以调用Promise.reject()

 Promise.reject({msg: 'Rejected!'}).catch(err => console.log(err)); 

让我提醒您: then在执行诺言时执行then处理程序,对于拒绝的诺言则执行catch处理程序。 但这还不是故事的结局。 在下面,我们将看到async/await如何与try/catch一起很好地工作。

8. ES6的组合者承诺:Promise.all,Promise.allSettled,Promise.any和其他


承诺并非旨在单独工作。 Promise API提供了多种组合诺言的方法。 最有用的一个是Promise.all ,它从promise获取一个数组并返回一个promise。 唯一的问题是,如果数组中至少有一个Promise被拒绝,Promise.all将被拒绝。

Promise.race在数组中的promise之一收到相应状态后立即允许或拒绝。

在V8的最新版本中,还将引入两个新的组合器: Promise.allSettledPromise.anyPromise.any仍处于提议的功能的早期阶段,在撰写本文时尚不支持。 但是,从理论上讲,他将能够发出信号,是否已执行任何诺言。 与Promise.race的区别在于,即使Promise.any之一被拒绝,也不会被Promise.any拒绝

Promise.allSettled更加有趣。 他还接受了一系列承诺,但是如果其中一个承诺被拒绝,他不会“缩短”承诺。 当您需要检查数组中的所有promise是否都经过某个阶段,而不管是否存在被拒绝的promise时,此功能很有用。 可以认为它与Promise.all相反。

9. ES6承诺和微任务队列


如果您还记得上一章,那么JavaScript中的每个异步回调函数在到达调用堆栈之前都位于回调队列中。 但是传递给promise的回调函数有不同的命运:它们由微任务队列而不是任务队列处理。

在这里,您需要注意: 微任务队列在调用队列之前 。 当事件循环检查新的回调是否准备好进入调用堆栈时,来自微任务队列的回调优先。

杰克·阿奇博尔德(Jake Archibald)在“ 任务,微任务,队列和日程安排 ,精彩阅读”中对此机制进行了更详细的描述。

10. JavaScript引擎:它们如何工作? 异步演进:从承诺到异步/等待


JavaScript正在迅速发展,我们每年都在不断改进。 Promises看起来像一个结局,但ECMAScript 2017(ES8)出现了新语法: async/await

async/await只是一种风格上的改进,我们称之为语法糖。 async/await不会以任何方式更改JavaScript(请不要忘记该语言应与旧版浏览器向后兼容,并且不应破坏现有代码)。 这只是一种基于Promise编写异步代码的新方法。 考虑一个例子。 上面,我们已经将诺言保存在相应的中:

 const myPromise = new Promise(function(resolve, reject) { resolve([{ name: "Chris" }]); }); myPromise.then((data) => console.log(data)) 

现在, 使用async/await我们可以处理异步代码,以便对我们清单的读者来说,这些代码看起来是同步的 。 除了使用then我们还可以将promise包装在一个名为async的函数中,然后我们将await结果:

 const myPromise = new Promise(function(resolve, reject) { resolve([{ name: "Chris" }]); }); async function getData() { const data = await myPromise; console.log(data); } getData(); 

看起来不错吧? 有趣的是,异步函数总是返回promise,没有人可以阻止它执行此操作:

 async function getData() { const data = await myPromise; return data; } getData().then(data => console.log(data)); 

错误呢? async/await的优点之一是这种构造可以让我们使用try/catch 。 阅读异步函数中的错误处理及其测试的介绍

让我们再次看一下promise,其中我们使用catch处理程序处理错误:

 const myPromise = new Promise(function(resolve, reject) { reject('Errored, sorry!'); }); myPromise.catch(err => console.log(err)); 

使用异步函数,我们可以这样重构:

 async function getData() { try { const data = await myPromise; console.log(data); // or return the data with return data } catch (error) { console.log(error); } } getData(); 

但是,并不是每个人都切换到这种样式。 try/catch会使您的代码复杂化。 还有一件事要考虑。 在以下代码中查看此try块内如何发生错误:

 async function getData() { try { if (true) { throw Error("Catch me if you can"); } } catch (err) { console.log(err.message); } } getData() .then(() => console.log("I will run no matter what!")) .catch(() => console.log("Catching err")); 

控制台中显示的两行呢? 请记住, try/catch是一个同步结构,而我们的异步函数会生成一个promise 。 他们遵循两条不同的路径,例如火车。 ! , throw , catch getData() . , «Catch me if you can», «I will run no matter what!».

, throw then . , , Promise.reject() :

 async function getData() { try { if (true) { return Promise.reject("Catch me if you can"); } } catch (err) { console.log(err.message); } } Now the error will be handled as expected: getData() .then(() => console.log("I will NOT run no matter what!")) .catch(() => console.log("Catching err")); "Catching err" // output 

async/await JavaScript. .

, JS- async/await . . , async/await — .

11. JavaScript-: ?


JavaScript — , , . JS-: V8, Google Chrome Node.js; SpiderMonkey, Firefox; JavaScriptCore, Safari.

JavaScript- «» : , , , . , .

JavaScript- , . JavaScript: , - , (, ) .

ECMAScript 2015 . — , . . 2017- async/await : , , .

Source: https://habr.com/ru/post/zh-CN452906/


All Articles