Javascript中并发编程与事件循环

Javascript单线程原因

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

事件循环原理

事件循环可视化描述:
eventLoop可视化描述
栈:执行栈
堆:存储对象
队列:消息队列或任务队列

所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
异步任务执行顺序:

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
  3. 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

只要主线程空了,就会去读取”任务队列”,这就是JavaScript的运行机制。

任务队列特点

“任务队列”是一个事件的队列(也可以理解成消息的队列)。
只要指定过回调函数,这些事件发生时就会进入”任务队列”,等待主线程读取。
主线程的读取过程基本上是自动的,只要执行栈一清空,”任务队列”上第一位的事件就自动进入主线程。但是,由于存在后文提到的”定时器”功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

永不阻塞

每一个消息执行完成后,其它消息才会被执行。
一个很有趣的事件循环 (event loop) 模型特性在于,Javascript 跟许多其它语言不同,它永不阻塞。通常由事件或者回调函数进行 I/O (input/output)处理 。所以当一个应用正等待IndexedDB 的查询的返回或者一个 XHR 的请求返回时,它仍然可以处理其它事情例如用户输入。

setTimeout和setInterval

setTimeout

调用 setTimeout 函数会在一个时间段过去后在队列中添加一个消息。这个时间段作为函数的第二个参数被传入。如果队列中没有其它消息,消息会被马上处理。但是,如果有其它消息,setTimeout消息必须等待其它消息处理完。因此第二个参数仅仅表示最少的时间而非确切的时间。setTimeout(fn,time)表示经过time时间插入到消息队列中,也就是任务队列中。
零延迟 (Zero delay) 并不是意味着回调会立即执行。在零延迟调用setTimeout时,其并不是过了给定的时间间隔后就马上执行回调函数。其等待的时间基于队列里正在等待的消息数量执行完成的时间。

setInterval

创建的定时器确保了定时器代码规则的插入到任务队列中。
问题是定时器代码可能在代码再次被添加到队列之前还没能完成执行,结果导致定时器代码连续运行好几次,而之间没有任何停顿。js引擎能避免这个问题,当使用setInteval()时,仅当没有该定时器的任何其他实例时,才将定时器代码添加到队列中。这确保了定时器代码加入到队列中最小时间间隔为指定间隔。
这个重复的定时器的规则有两个问题:

  1. 某些间隔会被跳过
  2. 多个定时器的代码执行之间的间隔比预期要小。

setInterval会产生回调堆积,特别是时间很短的时候。

setInterval Bug图
这个例子中的第 1 个定时器是在 205ms 处添加到队列中的,但是直到过了 300ms 处才能够执行。当执行这个定时器代码时,在 405ms 处又给队列添加了另外一个副本。在下一个间隔,即 605ms 处,第一个定时器代码仍在运行,同时在队列中已经有了一个定时器代码的实例。结果是,在这个时间点上的定时器代码不会被添加到队列中。结果在 5ms 处添加的定时器代码结束之后, 405ms 处添加的定时器代码就立刻执行。

避免这个两个缺点,可使用setTimeout链式调用

1
2
3
4
setTimeout(function(){
//处理中
setTimeout(arguments.callee, interval);
}, interval);

这个模式链式调用了setTimeout(),每次函数执行的时候都会创建一个新的定时器。第二个setTimeout()调用使用了arguments.callee来获取对当前执行的函数的引用,并为其设置另外一个定时器。这样做的好处是,在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。而且,它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。这个模式主要用于重复定时器。

参考文献