【译】理解Javascript函数执行—调用栈、事件循环、任务等
栏目: JavaScript · 发布时间: 7年前
内容简介:现如今,web开发者(我们更喜欢被叫做前端工程师)用一门脚本语言就能做任何事情,从提供浏览器中的交互,到开发电脑游戏、桌面工具、跨平台移动应用,甚至可以在服务端部署(如最流行的Node.js)来连结任意数据库。因此,了解Javascript的内部构造很重要,这样才能更优更高效的使用它。这也是本文的主旨所在。Javascript的生态正在变得越来越复杂。要构建一个现代web应用,会不可避免的用到Webpack、Babel、ESLint、Mocha、Karma、Grunt……我该用哪个?这些都是干嘛的?我找到
- 原文作者:Gaurav Pandvia
- 原文链接: medium.com/@gaurav.pan…
- 文中部分链接可能需要梯子。
- 欢迎批评指正。
现如今,web开发者(我们更喜欢被叫做前端工程师)用一门脚本语言就能做任何事情,从提供浏览器中的交互,到开发电脑游戏、桌面 工具 、跨平台移动应用,甚至可以在服务端部署(如最流行的Node.js)来连结任意数据库。因此,了解Javascript的内部构造很重要,这样才能更优更高效的使用它。这也是本文的主旨所在。
Javascript的生态正在变得越来越复杂。要构建一个现代web应用,会不可避免的用到Webpack、Babel、ESLint、Mocha、Karma、Grunt……我该用哪个?这些都是干嘛的?我找到了这个漫画,它完美诠释了如今的web开发者的水深火热:
在一头扎进框架和库的海洋之前,每个Javascript开发者首先需要了解Javascript在底层是如何实现的。差不多每个JS开发者都听过“V8”这个术语,但有些人可能根本不知道这个词到底什么意思、干嘛用的。在我职业开发生涯的第一年里,我对这些花里胡哨的术语所知甚少,我更关心先完成工作。但这样并不能满足我的好奇心,我好奇Javascript是他喵的怎么能做到这一切的。我决定要深挖一番,我翻遍Google,找到一些优秀的博客,包括Philip Roberts的 a great talk at JSConf on the event loop 。所以我决定总结我的学习经验并分享出来。鉴于有太多东西要了解,我把本文分为两个部分。这一部分会介绍常用术语,第二部分则会阐述这些术语之间的关联。
Javascript是一个单线程单并发的语言,也就是说它一次只能处理一个任务,执行一条代码。它的调用栈连同堆、队列一起构成了 Javascript并发模型 (在V8中实现)。让我们一个个地看这几个词。
- 调用栈(Call Stack) :它是记录我们在程序中调用函数的数据结构。假如我们调用一个函数来执行,就是在把某种记录推入到调用栈的顶端;当我们从一个函数中返回出来,就从调用栈顶端弹出记录。
当我们运行上图中的代码,我们会先寻找所有执行的开端——主函数。在上例中,一系列执行开始于 console.log(bar(6)) ,那么这一次执行就被推入调用栈中,它上面一层就是函数 bar 及其参数,函数 bar 转而调用函数 foo , foo 也被推入栈中;而 foo 随即 return 了某个值,所以被弹出调用栈;类似地, bar 随后弹出,最后 console 语句打印了结果并弹出。所有这些举动都依次发生在须臾之间。
你们肯定都在浏览器控制台见过那个又长又红的报错栈,它用一种从上到下的恰如栈的方式,简单表明了调用栈的当前状态以及在函数中何处报错(见下图)。
有时候,当我们以递归的形式多次调用一个函数,就会陷入无限循环中,而对于Chrome浏览器来说,它对调用栈的大小的限制是16000层,超出限制就会终止程序并抛出 达到栈上限错误 (见下图)。
- 堆 :对象会被分配到堆——内存中的松散结构。所有的针对变量和对象的内存分配都在堆中进行。
- 队列 :一种Javascript运行时,包含了一个消息队列,这个队列就是一系列将被处理的信息和要执行的相关回调函数。当调用栈有足够空间,就从队列中取出一条消息并进行处理,该消息调用相关联的函数(并因此产生一个初始化栈层)。当栈再次清空时,消息处理也就结束了。简单说,这些消息被排成队列,指定回调函数来响应外部异步事件(例如鼠标点击或HTTP请求的响应)。诸如用户点击按钮而没有相应回调函数的情况,就不会有消息放入队列中。
事件循环(event loop)
当我们评估JS代码的性能时,要知道调用栈中的函数会让程序或快或慢, console.log() 会很快,但用 for 或 while 迭代成千上万次就会慢一些,并且让调用栈一直被占用被阻塞着。这就叫做 阻塞脚本 ,你可能在 Webpage Speed Insights 中见过。
网络请求会慢,图片请求会慢,但万幸,服务请求可以通过AJAX这种异步函数完成。假如那些网络请求用同步函数来完成,将会如何?网络请求发送到服务器——服务器也就是某处的某种机器罢了,现在假设服务器返回响应可能会缓慢,此时,如果我点击一些CTA(call-to-action)按钮,或者其他一些需要完成的渲染,就不会有什么反应,因为调用栈还被之前的网络请求阻塞着。在 Ruby 等多线程语言中,这种情况可以控制,但像Javascript这种单线程语言,除非调用栈中的函数返回值,否则就一直堵着。浏览器没有任何反应,网页就会崩溃。这样我们可没办法为最终用户提供流畅的用户界面。那我们怎么办?
“JS中的并发——一次只做一件事,异步回调除外”
最早的解决方案就是用异步回调,这意味着我们给某部分代码加一个回调,该回调会在这段代码执行完成后执行。我们肯定都遇到过诸如AJAX请求用的 $.get() 、 setTimeout() 、 setInterval() 、 Promises 的异步回调。Node都是基于异步函数执行的。所有那些异步回调不会像 console.log() 等同步函数那样立刻运行,而是在之后的某个时刻运行,所以不会立刻就推到调用栈中去。那它们到底去哪里了?怎么控制它们?
如上例,若一个网络请求在Javascript中运行:
1. 请求函数被执行,给`onreadystatechange`事件传一个匿名函数作为回调,用来在将来响应就绪的时候执行。 2. “Script call done!”立刻输出到控制台。 3. 后续某时刻,响应被返回,回调被执行,响应体被输出到控制台。 复制代码
在等待异步操作完成并解除回调执行之时,响应的解耦调用允许Javascript运行时做别的事。浏览器插入进来调用了它的API,这是用C++实现的API,用来创建线程以控制诸如DOM事件、http请求、setTimeout等异步事件。
那些web接口不能自己把执行代码推入调用栈,如果能,那么该接口会随机出现在你的代码中(执行顺序不可控)。上面讨论过的消息回调队列说明了这一点。任何web接口在执行完毕后,都会把回调推入这个队列。 事件循环 此时就要负责控制队列中的回调的执行,并在栈空时把回调推入栈中。事件循环的基本工作就是监听调用栈和任务队列,当它看到栈空了,就把队列中第一个任务推入栈。每个消息或者回调都在上一个任务处理完再开始处理。
while (queue.waitForMessage()) {
queue.processNextMessage();
}
复制代码
在web浏览器中,一旦某事件发生并绑定了事件监听器,消息就立即添加到队列中。如果没有监听器,那就意味着事件丢失了。因此点击一个绑定了点击事件处理器,就会新增一个消息,其他事件亦如此。对其回调的调用将会是调用栈中的初始层,而由于Javascript是单线程的,在调用栈中所有调用都 return 之前,后续的消息的轮询和处理就暂停了。之后的(同步的)函数调用会向调用栈中增加新的调用层。
在下一部分,我会通过一个动画来展示上述过程的代码执行,深入解释什么是不同类型的异步函数、队列中谁优先执行,以及诸如零延迟等功能的技巧。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Python 函数调用&定义函数&函数参数
- Linux内核如何替换内核函数并调用原始函数
- gdb 如何调用函数?
- 汇编层面分析函数调用
- 理解 Golang 的函数调用
- Wasm 介绍(六):间接函数调用
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Game Engine Architecture, Second Edition
Jason Gregory / A K Peters/CRC Press / 2014-8-15 / USD 69.95
A 2010 CHOICE outstanding academic title, this updated book covers the theory and practice of game engine software development. It explains practical concepts and techniques used by real game studios,......一起来看看 《Game Engine Architecture, Second Edition》 这本书的介绍吧!