您是否想知道浏览器如何读取和执行JavaScript代码? 它看起来很神秘,但是在这篇文章中,您可以了解幕后发生的事情。
我们从游览JavaScript引擎的精彩世界开始我们的语言之旅。
在Chrome中打开控制台,然后转到“来源”标签。 您将看到几个部分,其中最有趣的部分是
调用堆栈 (在Firefox中,将断点放入代码中将看到调用堆栈):

什么是调用栈? 即使为了执行几行代码,这里似乎也发生了很多事情。 实际上,并不是所有浏览器都附带JavaScript。 有一个很大的组件可以编译和解释我们的JavaScript代码-这是一个JavaScript引擎。 最受欢迎的是V8,它用于Google Chrome和Node.js,Firefox中的SpiderMonkey,Safari / WebKit中的JavaScriptCore。
今天的JavaScript引擎是软件工程的典范,几乎不可能谈论所有方面。 但是,代码执行的主要工作仅由引擎的几个组件完成:调用堆栈(调用堆栈),全局内存(全局内存)和执行上下文(执行上下文)。 准备见他们吗?
内容:
- JavaScript引擎和全局内存
- JavaScript引擎:它们如何工作? 全局执行上下文和调用堆栈
- JavaScript是单线程的,还有其他有趣的故事
- 异步JavaScript,回调队列和事件循环
- 回调地狱并承诺ES6
- 创建和使用JavaScript Promises
- ES6承诺中的错误处理
- ES6 Promise组合器:Promise.all,Promise.allSettled,Promise.any和其他
- ES6承诺和微任务队列
- JavaScript引擎:它们如何工作? 异步演进:从承诺到异步/等待
- JavaScript引擎:它们如何工作? 总结
1. JavaScript引擎和全局内存
我说过,JavaScript既是编译语言,又是解释语言。 信不信由你,JavaScript引擎实际上在执行代码之前就会编译您的代码。
是某种魔术,对吧? 这种魔术称为JIT(及时编译)。 仅这是一个很大的讨论主题,甚至书籍也不足以描述JIT的工作。 但是现在,我们将跳过理论,而将重点放在执行阶段,这同样很有趣。
首先,请看以下代码:
var num = 2; function pow(num) { return num * num; }
假设我问您如何在浏览器中处理此代码? 你会怎么回答? 您可以说:“浏览器读取代码”或“浏览器执行代码”。 实际上,一切都不是那么简单。 首先,代码不是由浏览器读取,而是由引擎读取。
JavaScript引擎读取代码 ,并在定义第一行后立即将几个链接放入
全局内存 。
全局内存(也称为堆)是JavaScript引擎在其中存储变量和函数声明的区域。 当他阅读上面的代码时,两个绑定器将出现在全局存储器中:

即使示例仅包含变量和函数,也可以想象您的JavaScript代码是在更大的环境中执行的:在浏览器或Node.js中。 在这样的环境中,有许多预定义的函数和变量称为全局变量。 因此,请记住,全局内存将不仅包含
num
和
pow
,还包含更多的数据。
目前没有任何反应。 现在让我们尝试执行我们的功能:
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,您需要通过向其传递回调函数来调用构造函数。 它只能采用两个参数:
resolve
和
reject
。 让我们创建一个新的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
。 也就是说,以下代码将不起作用,并以允许的承诺结尾:
承诺现在看起来不太有用,对吧? 这些示例不向用户显示任何内容。 让我们添加一些东西。 允许的,被拒绝的承诺可以返回数据。 例如:
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) {
并且,如有必要,我们还可以通过调用
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.allSettled
和
Promise.any
。
Promise.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);
但是,并不是每个人都切换到这种样式。
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"
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
: , , .