「译」理解Javascript函数执行—调用栈、事件循环、任务等

现如今,web开发者(我们更喜欢被叫做前端工程师)用一门脚本语言就能做任何事情,从提供浏览器中的交互,到开发电脑游戏、桌面工具、跨平台移动应用,甚至可以在服务端部署(如最流行的Node.js)来连结任意数据库。因此,了解Javascript的内部构造很重要,这样才能更优更高效的使用它。这也是本文的主旨所在。

Javascript的生态正在变得越来越复杂。要构建一个现代web应用,会不可避免的用到Webpack、Babel、ESLint、Mocha、Karma、Grunt……我该用哪个?这些都是干嘛的?我找到了这个漫画,它完美诠释了如今的web开发者的水深火热:

image

Javascript疲劳症——学习Javascript是什么感觉

在一头扎进框架和库的海洋之前,每个Javascript开发者首先需要了解Javascript在底层是如何实现的。差不多每个JS开发者都听过“V8”这个术语,但有些人可能根本不知道这个词到底什么意思、干嘛用的。在我职业开发生涯的第一年里,我对这些花里胡哨的术语所知甚少,我更关心先完成工作。但这样并不能满足我的好奇心,我好奇Javascript是他喵的怎么能做到这一切的。我决定要深挖一番,我翻遍Google,找到一些优秀的博客,包括Philip Roberts的a great talk at JSConf on the event loop。所以我决定总结我的学习经验并分享出来。鉴于有太多东西要了解,我把本文分为两个部分。这一部分会介绍常用术语,第二部分则会阐述这些术语之间的关联。

Javascript是一个单线程单并发的语言,也就是说它一次只能处理一个任务,执行一条代码。它的调用栈连同堆、队列一起构成了Javascript并发模型(在V8中实现)。让我们一个个地看这几个词。

image

Visual Representation of JS Model

  1. 调用栈(Call Stack):它是记录我们在程序中调用函数的数据结构。假如我们调用一个函数来执行,就是在把某种记录推入到调用栈的顶端;当我们从一个函数中返回出来,就从调用栈顶端弹出记录。
image

JS Stack Visualization

当我们运行上图中的代码,我们会先寻找所有执行的开端——主函数。在上例中,一系列执行开始于console.log(bar(6)),那么这一次执行就被推入调用栈中,它上面一层就是函数bar及其参数,函数bar转而调用函数foo,foo也被推入栈中;而foo随即return了某个值,所以被弹出调用栈;类似地,bar随后弹出,最后console语句打印了结果并弹出。所有这些举动都依次发生在须臾之间。

你们肯定都在浏览器控制台见过那个又长又红的报错栈,它用一种从上到下的恰如栈的方式,简单表明了调用栈的当前状态以及在函数中何处报错(见下图)。

image

Error stack trace

有时候,当我们以递归的形式多次调用一个函数,就会陷入无限循环中,而对于Chrome浏览器来说,它对调用栈的大小的限制是16000层,超出限制就会终止程序并抛出达到栈上限错误(见下图)。

image
  1. :对象会被分配到堆——内存中的松散结构。所有的针对变量和对象的内存分配都在堆中进行。
  2. 队列:一种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()等同步函数那样立刻运行,而是在之后的某个时刻运行,所以不会立刻就推到调用栈中去。那它们到底去哪里了?怎么控制它们?

image

如上例,若一个网络请求在Javascript中运行:

<pre style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

1\. 请求函数被执行,给`onreadystatechange`事件传一个匿名函数作为回调,用来在将来响应就绪的时候执行。
2\. “Script call done!”立刻输出到控制台。
3\. 后续某时刻,响应被返回,回调被执行,响应体被输出到控制台。

</pre>

在等待异步操作完成并解除回调执行之时,响应的解耦调用允许Javascript运行时做别的事。浏览器插入进来调用了它的API,这是用C++实现的API,用来创建线程以控制诸如DOM事件、http请求、setTimeout等异步事件。

那些web接口不能自己把执行代码推入调用栈,如果能,那么该接口会随机出现在你的代码中(执行顺序不可控)。上面讨论过的消息回调队列说明了这一点。任何web接口在执行完毕后,都会把回调推入这个队列。事件循环此时就要负责控制队列中的回调的执行,并在栈空时把回调推入栈中。事件循环的基本工作就是监听调用栈和任务队列,当它看到栈空了,就把队列中第一个任务推入栈。每个消息或者回调都在上一个任务处理完再开始处理。

<pre style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

while (queue.waitForMessage()) {
 queue.processNextMessage();
}

</pre>

image

Javascript Event Loop Visual Representation

在web浏览器中,一旦某事件发生并绑定了事件监听器,消息就立即添加到队列中。如果没有监听器,那就意味着事件丢失了。因此点击一个绑定了点击事件处理器,就会新增一个消息,其他事件亦如此。对其回调的调用将会是调用栈中的初始层,而由于Javascript是单线程的,在调用栈中所有调用都return之前,后续的消息的轮询和处理就暂停了。之后的(同步的)函数调用会向调用栈中增加新的调用层。

在下一部分,我会通过一个动画来展示上述过程的代码执行,深入解释什么是不同类型的异步函数、队列中谁优先执行,以及诸如零延迟等功能的技巧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容