javascript 全局函数-JS引擎是如何工作的? 从调用堆栈到 Promise

2023-08-26 0 4,749 百度已收录

为了保证可读性,本文采用音译代替音译。

如果你想阅读更多优质文章,请戳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、微脉、青团社等多家品牌公司。 欢迎您免费试用!

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 javascript javascript 全局函数-JS引擎是如何工作的? 从调用堆栈到 Promise https://www.wkzy.net/game/162011.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务