为了保证可读性,本文采用音译代替音译。
如果你想阅读更多优质文章,请戳GitHub博客,一年有上百篇优质文章等着你!
有没有想过浏览器如何读取和运行 JS 代码? 这看起来很神奇,我们可以通过浏览器提供的控制台了解其背后的一些原理。
在Chrome中打开浏览器控制台,然后查看Sources一栏,可以在右侧找到一个CallStack包。
JS引擎是一个强大的组件,可以编译和解释我们的JS代码。 最流行的 JS 引擎是 Google Chrome 和 Node.js 使用的 V8、Firefox 的 SpiderMonkey 以及 Safari/WebKit 使用的 JavaScriptCore。
虽然JS引擎现在并没有为我们做所有的工作。 每个引擎中都有较小的部件为我们承担繁重的工作。
这些组件之一是调用堆栈 (CallStack),它与全局视频内存和执行上下文一起运行我们的代码。
JS引擎和全局内存(GlobalMemory)
JavaScript 既是编译语言又是解释语言。 不管你信不信,JS 引擎在执行代码之前只需要几毫秒的时间来编译代码。
听起来很神奇,对吧? 这些神奇的功能被称为JIT(即时编译)。 这是一个如此庞大的主题,以至于一本书不足以描述 JIT 的工作原理。 但现在,我们可以在中午跳过编译背后的理论,而专注于执行阶段,尽管这总是很有趣。
考虑以下代码:
var num = 2;
function pow(num) {
return num * num;
}
如果我问你如何在浏览器中处理上面的代码? 你打算说什么? 您可能会说“浏览器读取代码”或“浏览器执行代码”。
现实比这更加微妙。 首先,读取这段代码的不是浏览器,而是JS引擎。 JS 引擎读取代码,一旦到达第一行,它就会将多个引用加载到全局视频内存中。
全局显存(俗称堆)是JS引擎保存变量和函数声明的地方。 因此,回到前面的例子,JS引擎读取里面的代码时,会将两个绑定倒入全局显存中。
虽然示例只是变量和函数,但还要考虑 JS 代码运行的更大上下文:在浏览器中或在 Node.js 中。 在这个环境中,有很多预定义的函数和变量,称为全局变量。 全局内存会比num和pow多。
在上面的示例中,没有执行任何操作,如果我们像这样运行该函数会怎样:
var num = 2;
function pow(num) {
return num * num;
}
pow(num);
现在事情看起来很有趣。 当调用函数时,JavaScript 引擎为全局执行上下文和调用堆栈腾出空间。
JS 引擎:它们如何工作? 全局执行上下文和调用堆栈
刚刚学习了JS引擎如何读取变量和函数声明,最终将它们加载到全局显存(堆)中。
但是现在我们执行一个JS函数,JS引擎必须处理它。 怎么做? 每个 JS 引擎都有一个称为调用堆栈的基本组件。
调用栈是一种栈数据结构:这意味着元素可以从底部进入,但如果前面有元素则无法离开,这就是 JS 函数的原理。
一旦执行,其他函数如果保持阻塞状态,就无法离开调用堆栈。 请注意,这可以帮助您理解短语“JavaScript 是单线程的”。
回到我们的例子,当调用一个函数时,JS 引擎将该函数放入调用堆栈中
同时JS引擎还分配一个全局执行上下文,即运行JS代码的全局环境,如下所示
将全局执行上下文想象成一片海洋,全局函数像鱼一样游动,多么美妙! 但现实远没有这么简单,如果我的函数有一些嵌套变量或一个或多个内部函数怎么办?
即使进行如下简单的更改,JS 引擎也会创建一个本机执行上下文:
var num = 2;
function pow(num) {
var fixed = 89;
return num * num;
}
pow(num);
请注意,我在 pow 函数中添加了一个名为“fixed”的变量。 在这些情况下,会在 pow 函数中创建本地执行上下文,并将固定变量加载到 pow 函数中的本地执行上下文中。
引擎还为每个嵌套函数创建更多本地执行上下文。
JavaScript 是单线程的以及其他有趣的故事
JavaScript 是单线程的,因为只有一个调用堆栈处理我们的函数。 也就是说,如果还有其他函数等待执行,则函数无法离开调用堆栈。
当处理同步代码时,这不是问题。 例如,两个数字之间的总和以毫秒为单位同步。 但如果涉及异步怎么办?
幸运的是,JS 引擎默认是异步的。 虽然它一次执行一个函数,但还有一种方法可以让外部(例如:浏览器)执行较慢的函数,稍后将讨论该主题。
当浏览器加载各个JS代码时,JS引擎会逐行读取它们并执行以下步骤:
至此,我对JS引擎的同步机制有了基本的了解。 下一部分讲一下JS异步的工作原理。
异步JS、反弹队列和风暴循环
全局内存(堆)、执行上下文和调用堆栈解释了同步 JS 代码如何在浏览器中工作。 然而,我们遗漏了一些东西,当有一些异步函数运行时会发生什么?
请记住,调用堆栈一次只能执行一个函数,甚至阻塞函数也可以直接冻结浏览器。 幸运的是 JavaScript 引擎很聪明,但在浏览器的帮助下,它们可以解决问题。
当我们运行异步函数时,浏览器会获取该函数并运行它。 考虑以下代码:
setTimeout(callback, 10000);
function callback(){
console.log('hello timer!');
}
大家都知道setTimeout已经被使用过很多次了,但是你可能不知道它不是一个外部JS函数。 也就是说,JS出现的时候,语言中还没有外部的setTimeout。
setTimeout是浏览器API(BrowserAPI)的一部分,它是浏览器免费为我们提供的一套方便的工具。 这在实践中意味着什么? 因为setTimeout是浏览器的一个Api,所以该函数是由浏览器直接运行的(它会在调用堆栈中出现一段时间,但会立即被删除)。
10秒后,浏览器接受我们传入的回调函数,并将其连接到回调队列(CallbackQueu)。 .考虑以下代码
var num = 2;
function pow(num) {
return num * num;
}
pow(num);
setTimeout(callback, 10000);
function callback(){
console.log('hello timer!');
}
示意图如下:
正如您所看到的,setTimeout 在浏览器上下文中运行。 10 秒后,计时器启动,弹跳功能将运行。 但首先它要经过退回队列(CallbackQueue)。 弹跳队列是一种队列数据结构,弹跳队列是一个有序函数队列。
每个异步函数在被加载到调用栈之前都必须经过反弹队列,但是这个工作是谁来做的,那就是事件循环(EventLoop)。
风暴循环只有一项任务:检查调用堆栈是否为空。 如果反弹队列(CallbackQueue)中有函数,但调用堆栈空闲,则将其倒入调用堆栈。
完成后,执行该函数。 下面是用于处理异步和同步代码的 JS 引擎的图:
想象一下,callback() 已准备好执行。 当pow()完成时,调用堆栈(CallStack)为空,风暴循环(EventLook)将callback()加载到调用堆栈中。 就是这样,如果你理解了前面的插图,那么你就能理解所有的 JavaScript。
ES6 中的 Bounce Hell 和 Promise
Bounce 函数在 JS 中无处不在,它们在同步和异步代码中都使用。 考虑以下映射方法:
function mapper(element){
return element * 2;
}
[1, 2, 3, 4, 5].map(mapper);
映射器是在地图内部传递的反弹函数。 里面的代码是同步的,考虑异步的情况:
function runMeEvery(){
console.log('Ran!');
}
setInterval(runMeEvery, 5000);
代码是异步的,我们在setInterval中传递了反弹runMeEvery。 JS 中弹跳无处不在,因此存在一个问题:弹跳地狱。
JavaScript 中的反弹地狱是指一种编程风格,其中反弹嵌套在反弹函数中,而反弹函数又嵌套在其他反弹函数中。 由于 JS 的异步特性,多年来 js 程序员一直陷入这个陷阱。
说实话,我从来没有遇到过极端的反弹金字塔,这可能是由于我注重可读性的代码,但我始终坚持这个原则。 如果你遇到了反弹地狱,那么你的函数做得太多了。
我不会在这里讨论反弹地狱,但如果您好奇,有一个网站,callbackhell.com,它更详细地探讨了这个问题并提供了一些解决方案。
我们现在的重点是 ES6 Promise。 ES6Promises 是 JS 语言的补充,用于解决可怕的反弹地狱。 但什么是 Promise?
JS的Promise是未来风暴的代表。 承诺可以成功结束:用行话来说,我们已经解决(履行)。 但如果 Promise 失败,我们就说它被拒绝了。 Promise 也有一个默认状态:每个新的 Promise 都以待处理状态开始。
创建和使用 JavaScript Promise
要创建新的Promise,请通过传递bounce 函数来调用Promise 构造函数。 反弹函数可以接受两个参数:resolve 和reject。 如下:
const myPromise = new Promise(function(resolve){
setTimeout(function(){
resolve()
}, 5000)
});
如下所示,resolve是为了让Promise成功而被调用的函数,reject也可以用来表示调用失败。
const myPromise = new Promise(function(resolve, reject){
setTimeout(function(){
reject()
}, 5000)
});
请注意,在第一个示例中可以省略拒绝,因为它是第二个参数。 而且,如果您要使用拒绝,则不能忽略解决,如下所示,您最终将得到解决的承诺而不是拒绝。
// 不能忽略 resolve !
const myPromise = new Promise(function(reject){
setTimeout(function(){
reject()
}, 5000)
});
如今 Promise 看起来不太有用,我们可以向其中添加一些数据,如下所示:
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);
});
使用 Promise 进行错误处理
对于同步代码,JS错误处理大多很简单,如下:
function makeAnError() {
throw Error("Sorry mate!");
}
try {
makeAnError();
} catch (error) {
console.log("Catching the error! " + error);
}
将输出:
Catching the error! Error: Sorry mate!
现在尝试使用异步函数:
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 有一种处理异步错误的方法,就好像它们是同步的一样:
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));
Fundebug可以手动捕获JavaScript错误,包括Promise错误,欢迎免费试用~
Promise 的组成:Promise.all、Promise.allSettled、Promise.any
PromiseAPI 提供了多种组合 Promise 的方法。 其中最有用的是 Promise.all,它采用 Promise 的链接列表并返回 Promise。 如果参数中的一个 Promise 失败(被拒绝),则实例会反弹并失败(拒绝)javascript 全局函数,失败的原因是第一个失败的 Promise 的结果。
Promise.race(iterable) 方法返回一个承诺。 一旦迭代器中的 Promise 被解决或拒绝,返回的 Promise 也将被解决或拒绝。
较新版本的 V8 还将实现两个新组合:Promise.allSettled 和 Promise.any。 Promise.any 一直处于提案的早期阶段:截至撰写本文时,仍然没有浏览器支持它。
Promise.any 可以指示是否有任何 Promise 被完全履行。 与 Promise.race 的区别在于,如果 Promise 之一被拒绝,Promise.any 不会拒绝。
无论如何,这三个中最有趣的是 Promise.allSettled,它也是一个 Promise 字段,但如果其中一个 Promise 拒绝,它也不会漏电。 当你想要检测 Promise 的链接列表是否全部解决时,无论最终拒绝如何,它都非常有用javascript 全局函数,将其视为 Promise.all 的反对者。
异步演变:从 Promise 到 async/await
随着ECMAScript2017(ES8)的出现,引入了新的句型,async/await诞生了。
async/await 只是 Promise 语法糖。 它只是一种基于 Promises 编写异步代码的新方式,async/await 不会以任何方式改变 JS,记住,JS 必须向后兼容旧浏览器,并且不应该破坏现有代码。
我们举一个反例:
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();
有趣的是,异步函数也返回 Promises,所以你可以做同样的事情:
async function getData() {
const data = await myPromise;
return data;
}
getData().then(data => console.log(data));
那么如何处理错误呢? async/await 的一种用法是使用 try/catch。 再次查看 Promises,我们使用 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 时还有一个需要强调的怪癖,如下:
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 方法。
在实践中,我们不希望 throw 触及 then 处理程序。 一种解决方案是从函数返回 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" // 输出
此外,async/await 实际上是 JS 中构建异步代码的最佳方式。 我们对错误处理有更多的控制,并且代码看起来更干净。
总结
JS是一种Web脚本语言,具有先编译后由引擎解释的特点。 最流行的 JS 引擎包括 Microsoft Chrome 和 Node.js 使用的 V8、Firefox 构建的 SpiderMonkey 以及 Safari 使用的 JavaScriptCore。
JS引擎包含许多组件:调用堆栈、全局显存(堆)、事件循环和回调队列。 所有这些组件一起工作,并且经过完美调整,可以处理 JS 中的同步和异步代码。
JS 引擎是单线程的,这意味着只有一个调用堆栈来运行函数。 这种限制是 JS 异步本质的基础:所有需要时间的操作都必须由外部实体(例如浏览器)或反弹函数来处理。
为了简化异步代码流程,ECMAScript2015为我们带来了Promise。 Promise 是一个异步对象,用于表示任何异步操作的失败或成功。 但改进并不止于此。 2017 年,async/await 诞生了:它是 Promise 的风格补充,使编写异步代码成为可能,就像编写同步代码一样。
代码部署后可能存在的Bug无法实时得知。 事后为了解决此类bug,花费了大量的时间在日志调试上。 顺便给大家推荐一款好用的bug监控工具Fundebug。
关于 Fundebug
Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、ReactNative、Node.js和Java在线应用的实时BUG监控。 自2016年双11即将上线以来,Fundebug已累计处理超过10亿次错误事件。 付费客户包括阳光保险、核桃编程、荔枝FM、掌门1对1、微脉、青团社等多家品牌公司。 欢迎您免费试用!