JS引擎是如何工作的?从调用堆栈到Promise
摘要: 理解 js 引擎运行原理。
- 作者:前端小智
- 原文:js引擎:它们是如何工作的?从调用堆栈到promise,需要知道的所有内容
fundebug经授权转载,版权归原作者所有。
为了保证可读性,本文采用意译而非直译。
想阅读更多优质文章请猛戳github博客,一年百来篇优质文章等着你!
有没有想过浏览器如何读取和运行js代码? 这看起来很神奇,我们可以通过浏览器提供的控制台来了解背后的一些原理。
在chrome中打开浏览器控制台,然后查看sources这栏,在右侧可以到一个 call stack 盒子。
js 引擎是一个可以编译和解释我们的js代码强大的组件。 最受欢迎的js 引擎是v8,由 google chrome 和 node.j s使用,spidermonkey 用于firefox,以及safari/webkit使用的 javascriptcore。
虽然现在 js 引擎不是帮我们处理全面的工作。但是每个引擎中都有一些较小的组件为我们做繁琐的的工作。
其中一个组件是调用堆栈(call stack),与全局内存和执行上下文一起运行我们的代码。
js 引擎和全局内存(global memory)
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函数中会创建一个本地执行上下文,fixed
变量被放入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( browser api)的一部分,它是浏览器免费提供给我们的一组方便的工具。这在实战中意味着什么?由于settimeout
是一个浏览器的一个api,函数由浏览器直接运行(它会在调用堆栈中出现一会儿,但会立即删除)。
10秒后,浏览器接受我们传入的回调函数并将其移动到回调队列(callback queu)中。。考虑以下代码
var num = 2; function pow(num) { return num * num; } pow(num); settimeout(callback, 10000); function callback(){ console.log('hello timer!'); }
示意图如下:
如你所见,settimeout
在浏览器上下文中运行。 10秒后,计时器被触发,回调函数准备运行。 但首先它必须通过回调队列(callback queue)。 回调队列是一个队列数据结构,回调队列是一个有序的函数队列。
每个异步函数在被放入调用堆栈之前必须通过回调队列,但这个工作是谁做的呢,那就是事件循环(event loop)。
事件循环只有一个任务:它检查调用堆栈是否为空。如果回调队列中(callback queue)有某个函数,并且调用堆栈是空闲的,那么就将其放入调用堆栈中。
完成后,执行该函数。 以下是用于处理异步和同步代码的js引擎的图:
想象一下,callback()
已准备好执行,当 pow()
完成时,调用堆栈(call stack) 为空,事件循环(event look) 将 callback()
放入调用堆中。大概就是这样,如果你理解了上面的插图,那么你就可以理解所有的javascript了。
回调地狱和 es6 中的promises
js 中回调函数无处不在,它们用于同步和异步代码。 考虑如下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
。回调在js中无处不在,因此就会出现了一个问题:回调地狱。
javascript 中的回调地狱指的是一种编程风格,其中回调嵌套在回调函数中,而回调函数又嵌套在其他回调函数中。由于 js 异步特性,js 程序员多年来陷入了这个陷阱。
说实话,我从来没有遇到过极端的回调金字塔,这可能是因为我重视可读代码,而且我总是坚持这个原则。如果你在遇到了回调地狱的问题,说明你的函数做得太多。
这里不会讨论回调地狱,如果你好奇,有一个网站,,它更详细地探索了这个问题,并提供了一些解决方案。
我们现在要关注的是es6的 promises。es6 promises是js语言的一个补充,旨在解决可怕的回调地狱。但什么是 promises 呢?
js的 promise是未来事件的表示。 promise 可以以成功结束:用行话说我们已经解决了resolved(fulfilled)。 但如果 promise 出错,我们会说它处于拒绝(rejected )状态。 promise 也有一个默认状态:每个新的 promise 都以挂起(pending)状态开始。
创建和使用 javascript 的 promises
要创建一个新的 promise,可以通过传递回调函数来调用 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) });
注意,在第一个示例中可以省略reject
,因为它是第二个参数。但是,如果打算使用reject
,则不能忽略resolve
,如下所示,最终将得到一个resolved
的承诺,而非 reject
。
// 不能忽略 resolve ! const mypromise = new promise(function(reject){ settimeout(function(){ reject() }, 5000) });
现在,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); });
promises 的错误处理
对于同步代码而言,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错误,欢迎~
promises 组合:promise.all,promise.allsettled, promise.any
promise api 提供了许多将promise组合在一起的方法。 其中最有用的是promise.all,它接受一个promises数组并返回一个promise。 如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。
promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise
解决或拒绝,返回的 promise
就会解决或拒绝。
较新版本的v8也将实现两个新的组合:promise.allsettled和promise.any。 promise.any仍然处于提案的早期阶段:在撰写本文时,仍然没有浏览器支持它。
promise.any可以表明任何promise是否fullfilled
。 与 promise.race的区别在于promise.any不会拒绝即使其中一个promise
被拒绝。
无论如何,两者中最有趣的是 promise.allsettled,它也是 promise 数组,但如果其中一个promise拒绝,它不会短路。 当你想要检查promise
数组是否全部已解决时,它是有用的,无论最终是否拒绝,可以把它想象成promise.all 的反对者。
异步进化:从promises 到 async/await
ecmascript 2017 (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();
有趣的是,async
函数也会返回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
函数,我们可以重构以上代码:
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引擎中,有谷歌chrome和node.js使用的v8,有firefox构建的spidermonkey,以及safari使用的javascriptcore。
js引擎包含很有组件:调用堆栈、全局内存(堆)、事件循环、回调队列。所有这些组件一起工作,完美地进行了调优,以处理js中的同步和异步代码。
js引擎是单线程的,这意味着运行函数只有一个调用堆栈。这一限制是js异步本质的基础:所有需要时间的操作都必须由外部实体(例如浏览器)或回调函数负责。
为了简化异步代码流,ecmascript 2015 给我们带来了promise。 promise 是一个异步对象,用于表示任何异步操作的失败或成功。 但改进并没有止步于此。 在2017年,async/ await诞生了:它是promise
的一种风格弥补,使得编写异步代码成为可能,就好像它是同步的一样。
代码部署后可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的bug监控工具 fundebug。
关于fundebug
fundebug专注于javascript、微信小程序、微信小游戏、支付宝小程序、react native、node.js和java线上应用实时bug监控。 自从2016年双十一正式上线,fundebug累计处理了10亿+错误事件,付费客户有阳光保险、核桃编程、荔枝fm、掌门1对1、微脉、青团社等众多品牌企业。欢迎大家免费体验!
上一篇: Web网站实现Google登录
下一篇: Eclipse安装STS插件