浅谈 JavaScript 异步编程(一):JS 异步编程的含义

异步与同步

在介绍 JS 异步编程以前,我们首先得理清一下什么是异步,以及什么是并发。异步与同步是消息通信机制中两个相对的概念。

  • 同步:发出调用后,在得到结果之前,调用不返回。
  • 异步:发出调用后,调用直接返回,此时并没有获得结果。

让我们设想一个场景:有一天中午,我从教学楼里走出来,正想着去吃饭,突然一时兴起在图书馆里找一本书,于是拿出手机向图书管理员打电话。

高颜值的我与图书管理员在线对谈

接下来,我们分别用同步和异步去理解这个过程:

同步:

  1. 我打电话给图书管理员,提出请求(调用)。
  2. 我在电话里等图书管理员找书(等待结果)。
  3. 图书管理员完成找书,在电话里告诉我结果之后,我挂断电话(调用返回)。

异步:

  1. 我打电话给图书管理员,提出请求(调用)。
  2. 我挂断电话,骑车吃饭去了(调用返回)。
  3. 图书管理员完成找书,主动打电话告诉我结果(获得结果)。

这两个过程最显著的区别在于:我有没有干等着“找书”任务出结果

并发

并发是指一个系统支持两个或以上的任务同时存在。如果能够同时执行,这种并发也叫并行。不幸的是 JavaScript 本身是一门单线程的脚本语言,同一时间只能执行一个任务。单个 JavaScript 引擎实例无法做到并行。

不过,这并不意味着 JavaScript 不能借助外力来同时处理多个任务。还记得前面的漫画吗?我挂断电话后可以去食堂吃饭,但是此时找书的任务仍然在进行中。因为我将这个任务甩给了图书管理员,而他向我保证会在之后将结果告诉我。于是,我通过打电话给图书管理员这一异步操作,做到了一边找书一边吃饭。这种并发叫分时并发。

一边找书一边恰饭

JS 里的异步并发例子

类似地,在这段 JavaScript 代码中,我们要同时处理这两个任务:

  • 读取一个文件
  • 打印 do something else

我们的代码将读取文件的任务交给了 Node.js,然后打印了 do something else,最后 Node.js 读取文件完成,将最终结果输入 Callback 并交给 JS 引擎执行。

以上这个例子向我们粗略地阐述了 JS 中的异步与并发的概念。

为什么 JS 长于异步并发

今天,如果我们去翻开 JavaScript 的维基百科,上面会说它是一个“单线程非阻塞异步并发脚本语言”,那么为什么 JS 会以“异步并发”为特色呢?

历史原因

JS 自 1995 年诞生开始就是一门单线程的脚本语言,起初的设计目的只是辅助浏览器完成诸如验证表单这类简单的任务。并且,直到 Node.js 爆红之前,JS 的主要运行环境都是浏览器,所以它和浏览器之间存在着非常深的耦合关系。

在人们对提升 JS 性能的呼声越来越高时,浏览器支撑的庞大的 Web 体系不可能让 JavaScript 骤变变成一门支持多线程的语言来提高性能,所以 JavaScript 只能在单线程的道路上追求性能的极致。

环境原因

事实上在浏览器中,运行环境包括了 DOM、BOM 等浏览器自身暴露给 JavaScript 的 API,这些 API 通常由 C++编写,并且由浏览器执行,JS 只负责调用它们,也就是通知浏览器执行任务。

这一事实让人们找到了突破口:用支持多线程的运行环境为单线程的 JS 提供并发能力。于是,JS 变成了“大老板”,运行环境里的各种 C++线程成为了“打工仔”,专门为它服务。

JS 向运行环境发出若干个任务的指令,任务由运行环境完成并通知 JS,这一过程正是所谓的“异步并发”, 由此我们不难理解为什么“异步并发”是 JS 的一大特色了。

JS: like a boss

偏科的 JS

由此可见,JS 的设计使得它非常适合用于 I/O 密集型场合,因为这个“老板”能够高效地对若干个 C++线程发号施令,使得整个程序有条不紊地快速调度大量的 I/O 任务。

不过,它并不适合计算密集型场合,因为它只是一个单线程的“老板”,如果单看干活的多少,它显然不可能干得比两个人多。因此像 bcrypt 这样的加密库不得不像 I/O 相关的 API 那样借助多线程的 C++的力量来执行任务。

Node.js 在 Web 服务端领域占领的份额,其实主要集中在中间件等典型的高并发 I/O 密集型设施,其中一大关键原因就是因为 JS 的上述“偏科”缺陷。

另外,JS 单线程的设计也导致它难以充分利用多核 CPU 的性能,虽然 Node.js 能够通过 child_process 等方式实现这一点,但在 Web 服务端中的计算领域,相比其他成熟的方案(Java、C++等)优势并不明显,因此 Node.js 直到今天也难以撼动这些方案的地位。

面对Java无能狂怒的JS

链接