在 ES6 中,有一个新的概念建立在事件循环队列之上,叫作任务队列(job queue)。它是挂在事件循环队列的每个 tick 之后 的一个队列。在事件循环的每个 tick 中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会在当前 tick 的任务队列末尾添加一个项目(一个任务)。
事件循环队列类似于一个游乐园游戏:玩过了一个游戏之后,需要重新到队尾排队才能再玩一次。而任务队列类似于玩过了游戏之后,插队接着继续玩。
一个任务可能引起更多任务被添加到同一个队列末尾。所以,理论上说,任务循环(job loop)可能无限循环(一个任务总是添加另一个任务,以此类推),进而导致程序的饿死,无法转移到下一个事件循环 tick。从概念上看,这和代码中的无限循环(就像 while(true)
..)的体验几乎是一样的。
任务和 setTimeout(..0)
hack 的思路类似,但是其实现方式的定义更加良好,对顺序的保证性更强:尽可能早的将来。
设想一个调度任务(直接地,不要 hack)的 API,称其为 schedule(..)
。
console.log( "A" );
setTimeout( function(){
console.log( "B" );
}, 0 );
// 理论上的"任务API"
schedule( function(){
console.log( "C" );
schedule( function(){
console.log( "D" );
} );
} );
可能你认为这里会打印出 A B C D,但实际打印的结果是 A C D B。因为任务处理是在当前事件循环 tick 结尾处,且定时器触发是为了调度下一个事件循环 tick(如果可用的话!)。
实际上,JavaScript 程序总是至少分为两个块:第一块现在运行;下一块将来运行,以响应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访问,所以对状态的修改都是在之前累积的修改之上进行的。一旦有事件需要运行,事件循环就会运行,直到队列清空。事件循环的每一轮称为一个 tick。用户交互、IO 和定时器会向事件队列中加入事件。
任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接地引发一个或多个后续事件。