最近看了一些 javascript 异步编程方面文章, 也反复读了几遍薄薄的 << Async JavaScript >>。总结一下, 供自己后续学习使用, 并分享给大家。

首先, 有几个问题:

什么是异步编程 / 异步函数?
异步函数和回调函数有什么关系?
为什么异步编程经常与 javascript 同时出现?
javascript 中的异步函数的机制是怎样的?
那么现在异步编程有什么解决文案?
未来的 javascript 异步编程是什么样子?

什么是异步函数?

对一个 jser 而言,学习和使用 javascrtip 的过程中, 异步编程 出现频率应该是极高的,或许仅次于 事件驱动 / 单线程。那么什么是异步编程呢?什么是异步函数呢?

言简意赅的说:异步函数就是会导致将来运行 一个取自事件队列的函数 的函数。这是的重点是 取自事件队列,关于这个概念,暂且按下不表,将在后面进行分析,我们现在只需要知道异步函数是会导致将来某个时刻运行另外一个函数的函数。

异步函数 VS 回调函数

又是一个高频词汇,回调函数 。再次,我觉得有必要区分一下回调函数和异步函数的概念,虽然在很多人看来,在这一点上的区分不必太过纠结,可是借用老罗的话, 我不是为了输赢,我就是认真,对概念的精确理解和把握,往往是我们深入学习的第一个台阶。

所谓回函函数:

In computer programming, a callback is a piece of executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at some convenient time. The invocation may be immediate as in a synchronous callback or it might happen at later time, as in an asynchronous callback. In all cases, the intention is to specify a function or subroutine as an entity that is, depending on the language, more or less similar to a variable.(from wikipedia)

从 wikipedia 的说法中我们可以清晰的看到:

首先,回调函数作为参数传入到另外一段代码中的一段可执行代码,也就是它所强调的是回调函数是需要被当作参数传入到其它代码中的;

其次,回调函数可以是同步的,也可以是异步的,这取决于使用者。

如果我们进入到 wikipedia 的页面,我们能额外发现一些其它的知识,比如回调函数会出现在拥有某些特性的语言中,那么函数是一等会民的 javascript 当然也就完美支持回调函数了。

那么,现在这两个概念应该比较清晰了,我们举个例子比较一下。比如:

1
2
3
4
5
6
7
8
9
function callbackFunc() {
console.log("callback executed!");
}

setTimeout(callbackFunc, 10000);

function syncFunc(callbackFunc) {
callbackFunc();
}

在上面的代码片段中,setTimeou 是一个异步函数, 因为它导致 了大约 1 秒后 callbackFunc 的运行。而 callbackFunc 对于 setTimeout 来说,它是一个回调函数。同时,callbackFunc 对于 syncFunc 来说,它也是一个回调函数,但是被同步执行(在同一个事件循环里被执行), 那么 syncFunc 不能被称为异步函数

另外,在网上的一些文章中都能看到,很多人将回调函数作为了异步编程的一个解决文案进行总结,包括阮一峰老师的javascript 异步编程的 4 种方法。对于此,我认为这种分类是不恰当的。如果将回调函数看做异步编程的一种解决文案,那么我们后面讲到的分布式事件、Promise 以及强大的工作流程控制库都是借助回调函数的形式来实现,岂不是都能看作是同一种解决?所以,我认为,回调函数并不能简单地被当做异步编程的一种解决文案。

javascript 中的异步机制

每一个 jser 都应该了解,javascript 是单线程的,所谓 单线程,就是同一时刻只能执行一个任务,或者说只能有一个函数一个代码片段在执行。那么我们就很容易产生疑问,如果是单线程,那异步是如何实现的?

一句话回答:事件驱动(event-driven)

首先,javascript 是单线程执行的, 但是 javscript 引擎的平台(浏览器或者 nodejs)等是拥有若干线程的。比如,对于一个浏览器而言,有一条线程做渲染,有一条线程记录事件(click)等,有一条线程执行 javascript 等等,这些线程在浏览器内核的协调控制下执行(javascript 线程执行期间,不能进行 ui 渲染)。这是单线程实现的异步基础。

其次,每一个异步函数都会对应至少一个 event-handler,而上面提到的 事件队列 便是 event-handler 在被处理的时候该放在的地方。javascript 引擎的线程会在适当的时机处理一系列的 event-handler,适当的时机需要满足两个条件:

  1. 该事件已经满足触发条件(比如:setTimeout(func, 1000)后大约 1000ms);
  2. javascript 的线程空闲(比如:setTimeout 注册的回调延时条件已经满足,但是此时的 javascript 引擎正在做一个复杂的 for 循环耗时 3 秒,那么 setTimeout 的回调函数也能只能等待 for 循环执行完成后,再执行)。

这是再提到 event-loop 的存在,每一次循环都是一个 tick,它的作用就是在不断循环检测事件队列中是否有 event-handler,如果有便会取出执行。我们可以这样理解 event-loop:

1
2
3
4
5
while (true) {
if (atLeasetOneEventIsQueued) {
fireNextQueuedEvent();
}
}

最后,事件满足触发条件(上文中适当的时机条件 1)是如何判断的?

不同的事件的触发条件可能由不同的线程监控。比如,我们发送一个 ajax 请求,应该是浏览器有一个独立的线程发送 http 请求并在请求返回的时候通知 javascript 引擎线程满足触发条件;而 click 一个 button,应该是浏览器的 GUI 线程通知 javascript 引擎,然后适时执行相应的 event-handler。

我们举个例子说明,假设:我们处在一个页面,这个页面上有一个 setTimeout 正在执行延时 1000ms 执行的某段代码 ;而在这个 200ms 的时候,我们点击了一个按钮,因为此时已经满足事件触发条件,且 javscript 线程空闲,所以按照我们脚本,浏览器会立即执行与这个事件绑定的另外某段代码;点击事件触发的某段代码会做两件事,

  • 一件是注册一个 setinterval 要求每隔 700ms 执行某段代码
  • 另一件是发送一个 ajax 请求,并要求请求返回后执行某段代码,这个请求会在 1500ms 后返回。在这之后可能还会有其它事件被触发。

上文中,每一个某段代码都是一个 event-handler, 而 event-handler 被触发的时机可能受前面 event-handler 的影响。我们按照每个 event-handler 的执行时间都非常短来处理。可以提到下图(上文标示 event-hanlder 对应的异步函数,下方标示大致的时间):

从图中我们能看到事件的执行顺序,这个很容易理解。现在想一下,如果点事件的 event-handler 先执行一个 while 循环耗时了 100ms,然后再去 setInterval 和 ajax 请求,那么执行的顺序又是怎样的呢?如果理解了 javascript 的事件驱动机制,这个就很容易了。留下一段代码,大家自己尝试一下。是不是跟大家想的一样?

1
2
3
4
5
6
var obj = {"num": 1}, start = new Date;
setTimeout(function() {obj.num = 2}, 0);

while(new Date - start < 1000) {}

alert(JSON.stringify(obj));

或许还可以想到我们平时遇到的一些问题背后的原因:

  1. 为什么大多情况下 setInterval 执行间隔会小于 setTimeout?
  2. 为什么 setTimeout 会有最小间隔?whatwg 和 w3c 的 HTML5 规范都规定 4ms
  3. 为什么建议耗时的函数分多次执行?比如,process.nextTick。

本文地址 https://shaoshilei.com/2016-05/async-javascript.html