最近看了一些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对应的异步函数,下方标示大致的时间):

1

从图中我们能看到事件的执行顺序,这个很容易理解。现在想一下,如果点事件的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。

本文地址 http://laoono.com/2016-05/async-javascript.html