• 原文作者:Gaurav Pandvia
  • 原文链接:medium.com/@gaurav.pan…
  • 文中部分链接可能需要梯子。
  • 欢迎批评指正。

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

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

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

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

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

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

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

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

Error stack trace

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

  1. :对象会被分配到堆——内存中的松散结构。所有的针对变量和对象的内存分配都在堆中进行。
  2. 队列:一种Javascript运行时,包含了一个消息队列,这个队列就是一系列将被处理的信息和要执行的相关回调函数。当调用栈有足够空间,就从队列中取出一条消息并进行处理,该消息调用相关联的函数(并因此产生一个初始化栈层)。当栈再次清空时,消息处理也就结束了。简单说,这些消息被排成队列,指定回调函数来响应外部异步事件(例如鼠标点击或HTTP请求的响应)。诸如用户点击按钮而没有相应回调函数的情况,就不会有消息放入队列中。

事件循环(event loop)

当我们评估JS代码的性能时,要知道调用栈中的函数会让程序或快或慢,console.log()会很快,但用forwhile迭代成千上万次就会慢一些,并且让调用栈一直被占用被阻塞着。这就叫做阻塞脚本,你可能在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();
}
复制代码
Javascript Event Loop Visual Representation

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

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

【译】理解Javascript函数执行—调用栈、事件循环、任务等相关推荐

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

    现如今,web开发者(我们更喜欢被叫做前端工程师)用一门脚本语言就能做任何事情,从提供浏览器中的交互,到开发电脑游戏.桌面工具.跨平台移动应用,甚至可以在服务端部署(如最流行的Node.js)来连结任 ...

  2. [译]深入理解JavaScript函数执行—调用栈,事件循环和任务等

    Web 开发者,或者前端工程师(我们更喜欢别人这么称呼)现如今几乎能做所有的工作,从扮演一个浏览器内部交互性的角色,到制作电脑游戏.桌面控件.跨平台手机应用,甚至还可以把它写在服务器端(最流行的是no ...

  3. 深入理解javascript函数系列第二篇——函数参数

    前面的话 javascript函数的参数与大多数其他语言的函数的参数有所不同.函数不介意传递进来多少个参数,也不在乎传进来的参数是什么数据类型,甚至可以不传参数.本文是深入理解javascript函数 ...

  4. 2020-08-14 理解 javascript 函数中的 curry

    理解 javascript 函数中的 curry 函数柯里化: 所谓函数柯里化就是把一个函数的多个传参变成多个函数的单个传参. 函数的柯里化,是 Javascript 中函数式编程的一个重要概念.它返 ...

  5. JavaScript执行机制-node事件循环

    node环境下的事件循环机制 和浏览器有什么不同? 在node中,事件循环表现出来的状态和浏览器大致相同,但是node有一套自己的模型. node事件循环依靠libuv引擎,node选择chrome ...

  6. 理解JavaScript的执行机制

    一直没有深入了解过JavaScript的事件执行机制,直到看到了这篇文章:<这一次,彻底弄懂JavaScript执行机制> 才发觉熟悉JavaScript的执行机制非常重要. 毕竟在跟进项 ...

  7. 深入理解JavaScript的闭包特性如何给循环中的对象添加事件

    初学者经常碰到的,即获取HTML元素集合,循环给元素添加事件.在事件响应函数中(event handler)获取对应的索引.但每次获取的都是最后一次循环的索引.原因是初学者并未理解JavaScript ...

  8. 深入理解Javascript之执行上下文(Execution Context)

    在这篇文章中,将比较深入地阐述下执行上下文 - Javascript中最基础也是最重要的一个概念.相信读完这篇文章后,你就会明白javascript引擎内部在执行代码以前到底做了些什么,为什么某些函数 ...

  9. Javascript函数执行、new机制以及继承

    JS函数执行 一个JavaScript函数fn,被执行有三种途径: fn() new fn() fn.call()或fn.apply() new机制以及继承 JavaScript中定义了一种对象,称之 ...

最新文章

  1. 熬10天夜,肝出了这个PDF版“软件安装手册”(附下载)
  2. c语言 得到结构体成员偏移
  3. Android数据存储方式
  4. 探寻新的治疗方法,研究人员用VR可视化DNA结构
  5. 谷歌系列 :Inception v1到v4
  6. Python:在Pandas数据框中查找缺失值
  7. mysql客户端 mariadb_配置MariaDB允许客户端远程连接
  8. 【数据库系统设计】数据库安全性
  9. linux c代码调试工具,在 Linux 中调试 C 程序的福音——gdb
  10. js调用局部打印功能并还原
  11. JS 数据容量转换/换算
  12. 英语之形容词和副词规则
  13. word表格分开快捷键_Word使用技巧(表格技巧、快捷键)
  14. 福大软工 · 第七次作业 - 需求分析报告
  15. Qt与flash交互实现(播放Flash动画)
  16. 群体智能与进化计算_群智能计算简介
  17. 界门纲目科属种的英文——学生物的基础
  18. React 基础学习
  19. Android Vibrator
  20. DebugView使用方法

热门文章

  1. 00后当道,今秋开学的AI专业的本科生究竟学什么?
  2. 突发 | Yann LeCun卸任!Facebook变天,做AI不能落地是不成了
  3. CTO 写的代码,真是绝了
  4. Redis + Tomcat + Nginx 集群实现 Session 共享
  5. Redis实现分布式锁的深入探究
  6. IntelliJ IDEA 2019从入门到癫狂 图文教程!
  7. 那些年,我们见过的 Java 服务端乱象
  8. 懂点 Nginx 反向代理与负载均衡,是面试加分项没有之一
  9. 工作之余如何提高个人技术水平
  10. 2023 USNews全美计算机研究生院排名发布!MIT、CMU分别称霸总榜和AI分榜