原文地址javascript
有没有想过浏览器如何读取和运行JavaScript代码? 这看起来很神奇,但你能够获得一些发生在幕后的线索。java
让咱们经过介绍JavaScript引擎的精彩世界来沉浸在其语言之中。程序员
在Chrome中打开浏览器控制台,而后查看“Sources”标签。 你会看到一些盒子,其中有趣的一个叫Call Stack(在Firefox中,你能够在代码中插入一个断点后看到调用堆栈):es6
什么是调用堆栈? 彷佛有不少事情要讲,即便它运行几行代码。 事实上,对Web浏览器JavaScript并非开箱即用的。编程
编译和解释JavaScript代码是重要的一部分,这就是JavaScript引擎。最流行的JavaScript引擎是V8,谷歌Chrome和Node.js,使用SpiderMonkey的Firefox,使用JavaScriptCore的Safari / WebKit。数组
今天的,avaScript引擎是伟大的项目,并无涵盖它们的每一个方面。每一个引擎机的工做内容中都有一些小部分,对咱们来讲很难。promise
其中一个组件是调用堆栈,它与全局内存和执行上下文一块儿使运行咱们的代码成为可能。准备好了解它们?浏览器
我提到JavaScript既是编译语言同时又是解释语言。信不信由你,JavaScript引擎在执行以前实际上只用几微妙编译了你的代码。bash
这听起来很神奇吗? 它被称做JIT(即时编译)。这自己就是一个很大的话题,另外一本书不足以描述JIT的工做原理。可是如今咱们能够跳过编译背后的理论,并专一于执行阶段,这并不会减小乐趣。网络
首先考虑如下代码:
var num = 2;
function pow(num) {
return num * num;
}
复制代码
若是我问你如何在浏览器中处理上述代码?你会说些什么?你可能会说“浏览器读取代码”或“浏览器执行代码”。
现实比那更微妙。首先,浏览器不是读取该代码片断。这是引擎。JavaScript引擎读取代码,一旦遇到第一行,它就会将一些引用放入全局内存中。
**全局内存(也称为Heap)**是JavaScript引擎保存变量和函数声明的区域。因此,回到咱们的例子,当引擎读取上面的代码时,全局内存中填充了两个绑定:
var num = 2;
function pow(num) {
return num * num;
}
pow(num);
复制代码
将会发生什么?如今事情变得有趣了。当一个函数被调用时,JavaScript引擎为另外两个盒子腾出空间:
让咱们看看它们在下一节中的含义。
你了解了JavaScript引擎如何读取变量和函数声明。它们最终在全局内存(Heap)中结束。
可是如今咱们执行了一个JavaScript函数,引擎必需要处理它。怎么办?每一个JavaScript引擎都有一个基本组件,叫作调用栈。
调用栈是一个栈数据结构:这意味着元素能够从顶部进入,但若是它们上面有一些元素,它们就不能离开。JavaScript函数就是这样的。
一旦执行,若是某些其余功能仍然卡住,则没法离开调用堆栈。 请注意,在脑海中记着“JavaScript是单线程”有助于理解这个概念。。
可是如今让咱们回到咱们的例子。调用该函数时,引擎会在调用堆栈中推送该函数:
我喜欢将Call Stack视为一桶薯片。若是没有先吃掉顶部的薯片,就不能吃到底部的薯片!幸运的是咱们的功能是同步的:它是一个简单的乘法,它能够快速计算出来。
同时,引擎还分配了一个全局执行上下文,这是咱们运行JavaScript代码的全局环境。这是它的样子:
想象全局执行上下文是个海洋,其中JavaScript全局函数像鱼同样游动。如此美妙!但那只是故事的一半。若是咱们的函数有一些嵌套变量或一个或多个内部函数怎么办?
即便在以下的简单变体中,JavaScript引擎也会建立本地执行上下文:
var num = 2;
function pow(num) {
var fixed = 89;
return num * num;
}
pow(num);
复制代码
请注意,我在函数pow中添加了一个名为fixed的变量。在这种状况下,本地执行上下文将包含一个用于保持固定的盒子。
我不太擅长在其余小盒子里画小小的盒子!你不得不用你的想象力。
本地执行上下文将出如今 pow 附近,包含在全局执行上下文中的绿色框内。还能够想象,对于嵌套函数的每一个嵌套函数,引擎都会建立更多本地执行上下文。 这些盒子能够很快到达目的地!像俄罗斯套娃!
如今回到单线程故事怎么样? 这是什么意思?
咱们说 JavaScript是单线程的,由于有一个Call Stack处理咱们的函数。也就是说,若是有其余函数等待执行,函数不能离开调用堆栈。
处理同步代码时,这不是问题。例如,两个数字之间的和是同步的,并以微秒为单位运行。可是网络调用和与外界的其余互动怎么办?
幸运的是,JavaScript引擎默认设计为异步。即便他们一次能够执行一个函数,也有一种方法可让外部实体执行较慢的函数:浏览器就是一个例子。咱们稍后会探讨这个话题。
在此期间,了解到当浏览器加载某些JavaScript代码时,引擎会逐行读取并执行如下步骤:
到目前为止,您应该已经了解了每一个JavaScript引擎基础上的同步机制。在接下来的部分中,您将看到异步代码在JavaScript中的工做原理以及它为什么如此工做。
全局内存,执行上下文和调用堆栈解释了同步JavaScript代码在浏览器中的运行方式。 然而,咱们错过了一些东西。 当有一些异步函数运行时会发生什么?
经过异步函数,与外界的每次互动都须要一些时间才能完成。 调用REST API或调用计时器是异步的,由于它们可能须要几秒钟才能运行。 使用咱们到目前为止在引擎中的元素,如今有办法处理这种函数而不会阻塞调用堆栈,浏览器也是如此。
请记住,调用堆栈一次能够执行一个函数,甚至一个阻塞函数也能够直接冻结浏览器。 幸运的是JavaScript引擎是聪明的,而且在浏览器的帮助下能够解决问题。
当咱们运行异步函数时,浏览器会获取该函数并为咱们运行它。 考虑以下的计时器:
setTimeout(callback, 10000);
function callback(){
console.log('hello timer!');
}
复制代码
我肯定你看过setTimeout
数百次,但你可能不知道它不是内置的JavaScript函数。也就是说,当JavaScript诞生时,语言中没有内置的setTimeout。
事实上,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秒后,计时器被触发,回调功能准备好运行。但首先它必须经过回调队列。**回调队列是一个队列数据结构,顾名思义是一个有序的函数队列。
每一个异步函数在被推入调用堆栈以前必须经过回调队列。但谁推进了这个功能?还有另外一个名为Event Loop的组件。
Event Loop如今只有一个工做:它应检查Call Stack是否为空。 若是回调队列中有一些功能,而且若是调用堆栈是空闲的,那么是时候将回调推送到调用堆栈。
完成后,执行该功能。 这是用于处理异步和同步代码的JavaScript引擎的大图:
想象一下,callback()已准备好执行。 当pow()完成时,Call Stack为空,Event Loop推送callback()。就是这样! 即便我简化了一些事情,若是你理解了上面的插图,那么你就能够理解全部的JavaScript了。
请记住:浏览器API,回调队列和事件循环是异步JavaScript的支柱。
若是您喜欢视频,我建议观看Philip Roberts不管如何都要看事件循环。这是Event Loop有史以来最好的解释之一。
坚持下去,由于异步JavaScript尚未完成。在接下来的部分中,咱们将详细介绍ES6 Promises。
回调函数在JavaScript中无处不在。 它们用于同步和异步代码。考虑map方法,例如:
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 Promises。ES6 Promises是JavaScript语言的补充,旨在解决可怕的回调地狱。但不管如何,Promise是什么?
JavaScript Promise是将来事件的表示。承诺能够以成功结束:用行话说咱们已经解决了(履行)。但若是Promise出错,咱们会说它处于拒绝状态。 Promise也有一个默认状态:每一个新的Promise都以挂起状态开始。能够建立本身的Promise吗?是。让咱们进入下一节看看如何作。
要建立新的Promise,能够经过将回调函数传递给它来调用Promise构造函数。回调函数能够采用两个参数:resolve和reject。让咱们建立一个新的Promise,它将在5秒后解析(您能够在浏览器的控制台中尝试这些示例):
const myPromise = new Promise(function(resolve){
setTimeout(function(){
resolve()
}, 5000)
});
复制代码
正如您所看到的,resolve 是一个函数,为咱们成功调用Promise而生。Reject 相反地产生一个 rejected Promise:
const myPromise = new Promise(function(resolve, reject){
setTimeout(function(){
reject()
}, 5000)
});
复制代码
请注意,在第一个示例中,您能够省略拒绝,由于它是第二个参数。但若是你打算使用 reject,你就不能省略 resolve。换句话说,如下代码将没法工做,最终将以已解决的Promise结束:
//不能省略resolve!
const myPromise = new Promise(function(reject){
setTimeout(function(){
reject()
}, 5000)
});
复制代码
如今,Promise看起来不那么有用不是吗?这些示例不向用户打印任何内容。让咱们添加一些数据。resolved 和 rejected 的 Promises 均可以返回数据。这是一个例子:
const myPromise = new Promise(function(resolve) {
resolve([{ name: "Chris" }]);
});
复制代码
但咱们仍然看不到任何数据。要从Promise中提取数据,须要连接一个名为then的方法。它须要一个回调(具备讽刺意味的!)来接收实际数据:
const myPromise = new Promise(function(resolve, reject) {
resolve([{ name: "Chris" }]);
});
myPromise.then(function(data) {
console.log(data);
});
复制代码
做为JavaScript开发人员和其余人代码的消费者,将主要与来自外部的Promises进行交互。相反,库建立者更有可能将遗留代码包装在Promise构造函数中,以下所示:
const shinyNewUtil = new Promise(function(resolve, reject) {
// do stuff and resolve
// or reject
});
复制代码
在须要时,咱们还能够经过调用 Promise.resolve()
来建立和解决Promise:
Promise.resolve({ msg: 'Resolve!'})
.then(msg => console.log(msg));
复制代码
所以,回顾一下JavaScript Promise是将来发生的事件的书签。事件以挂起状态启动,能够成功(已解决,已履行)或失败(已拒绝)。 Promise能够返回数据,而后经过附加到Promise来提取数据。在下一节中,咱们将看到如何处理来自Promise的错误。
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仅适用于同步代码。若是你很好奇,Node.js中的错误处理会详细解释这个问题。
幸运的是,Promise有一种处理异步错误的方法,就像它们是同步的同样。若是你回忆起上一节中的 reject, 产生了一个拒绝的 Promise:
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:
Promise.reject({msg: 'Rejected!'}).catch(err => console.log(err));
复制代码
回顾一下:当一个Promise被填满时,then处理程序运行,而catch处理程序运行被拒绝的Promises。但这不是故事的结局。稍后咱们将看到async / await如何与try / catch很好地协做。
Promises 并不意味着孤军奋战。 Promise API 提供了许多将 Promise 组合在一块儿的方法。其中最有用的是Promise.all,它接受一个 Promises 数组并返回一个Promise。当数组中的任何 Promise 是 reject 时,Promise.all 返回 reject。
只要数组中的一个Promise 有结果,Promise.race 就会是 resolves 或者 reject。若是其中一个 Promise 拒绝,它仍然会 reject。
较新版本的V8也将实现两个新的组合器:Promise.allSettled 和 Promise.any。 Promise.any仍然处于提案的早期阶段:在撰写本文时,仍然没有人支持它。
但理论是Promise.any能够代表任何Promise是否都知足 resolve。与Promise.race的区别在于Promise.any不会reject,即便其中一个Promise 是 reject。
不管如何,二者中最有趣的是Promise.allSettled。它仍然须要一系列Promise,但若是其中一个Promise 是 reject,它不会短路。当您想要检查Promise数组是否所有返回,它是有用的,不管最终是否是拒绝。把它想象成 Promise.all 地对立面。
若是你记得之前的部分,**JavaScript中的每一个异步回调函数都会在被推入调用堆栈以前在回调队列中结束。**可是在Promise中传递的回调函数有不一样的命运:它们由Microtask Queue(微任务队列)处理,而不是由Callback Queue(回调队列)处理。
你应该注意一个有趣的怪癖:微任务队列优先于回调队列。当事件循环检查是否有任何新的回调准备好被推入调用堆栈时,来自微任务队列的回调具备优先权。
Jake Archibald在任务,微任务,队列和日程安排中更详细地介绍了这些机制,这是值得一读。
JavaScript正在快速发展,每一年咱们都会不断改进语言。 Promises彷佛是到达点,可是在ECMAScript 2017(ES8)中诞生了一种新的语法:async / await。
async/await只是一种风格上的改进,咱们称之为语法糖。 async/await不会以任何方式改变JavaScript(请记住,JavaScript必须向后兼容旧浏览器,不该破坏现有代码)。
它只是一种基于Promises编写异步代码的新方法。让咱们举个例子。以前咱们用相应的保存Promise:
const myPromise = new Promise(function(resolve, reject) {
resolve([{ name: "Chris" }]);
});
myPromise.then((data) => console.log(data))
复制代码
如今 使用 async/await,咱们能够从读者的角度看待同步的异步代码。 咱们能够将Promise包装在标记为async的函数中,而后等待结果:
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/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引起的错误永远不会触发 getData()的 catch 处理程序。 运行上面的代码将致使“Catch me if you can”,而后是“I will run no matter what!”。
在现实世界中,咱们不但愿throw触发当时的处理程序。 一种可能的解决方案是从函数返回Promise.reject():
async function getData() {
try {
if (true) {
return Promise.reject("Catch me if you can");
}
} catch (err) {
console.log(err.message);
}
}
复制代码
如今错误处理是预期的那样了:
getData()
.then(() => console.log("I will NOT run no matter what!"))
.catch(() => console.log("Catching err"));
"Catching err" // output
复制代码
除此以外,async / await彷佛是在JavaScript中构建异步代码的最佳方式。咱们能够更好地控制错误处理,代码看起来更清晰。
不管如何,我不建议将全部JavaScript代码重构为async / await。这些是必须与团队讨论的选择。可是若是你单独工做,不管你使用简单的Promises, 仍是 async/await 它都是我的偏好的问题。
JavaScript 是一种用于Web的脚本语言,具备首先编译而后由引擎解释的特性。 在最流行的JavaScript引擎中,有谷歌Chrome和Node.js使用的V8,为网络浏览器Firefox构建的SpiderMonkey,以及Safari使用的JavaScriptCore。
JavaScript引擎有不少使人激动的部分:调用堆栈,全局内存,事件循环,回调队列。全部这些部分在完美调整中协同工做,以便在JavaScript中处理同步和异步代码。
JavaScript引擎是单线程的,这意味着有一个用于运行函数的Call Stack。这种限制是JavaScript异步性质的基础:全部须要时间的操做必须由外部实体(例如浏览器)或回调函数负责。
为了简化异步代码流,ECMAScript 2015给咱们带来了 Promise。Promise是一个异步对象,用于表示任何异步操做的失败或成功。但改进并无止步于此。在2017年,async/await诞生了:它是Promise的一种风格弥补,使得编写异步代码成为可能,就好像它是同步的同样。
感谢阅读并敬请关注此博客!