事件的调度
可怕的总是那些细节
像 setTimeout这样看起来很简单的东西,经常的描述是这样:“给定一个回调及n毫秒的延迟,setTimeout就会在n毫秒后运行该回调。”。这种描述存在严重的缺陷,大多数情况下,这样的描述只能算是接近正确,而在其他情况下则完全是谬误。想要真正理解 setTimeout 必须先理解 JS 事件模型。
现在还是将来运行
for(var i=1; i<=3; i++){
setTimeout(function(){
console.log(i);
}, 0);
}
答案是什么呢?要理解为什么输出是4 4 4,需要知道一下3件事儿:
- 只有一个名为
i的变量,其作用域由声明语句var i定义,该声明语句在不经意间让i的作用不是循环内部,而是扩散至蕴含循环的哪个最内侧函数。 - 循环结束后,
i===4一直递增,直到不在满足条件i<=3为止。 - JS事件处理器在线程空闲之前不会运行
线程的堵塞
var start = new Date;
setTimeout(function(){
var end = new Date;
console.log('time elapsed:', end-start, 'msg');
}, 500);
while(new Date - start < 1000){
}
按照多线程的思维定式,预计500毫秒后计时器就会运行。不过这要求中断要持续整整1000毫秒的循环。由于 setTimeout 和 setInterval 一样,计时精度要比期望值差很多。不过数字至少是1000,因为setTimeout 回调在 while 循环之前不可能被触发。那么,如果 setTimeout 没有使用另一个线程,那它到底做什么呢?
队列
调用 setTimeout 的时候,会有一个延时事件排入队列。然后 setTimeout 调用之后的代码运行,接着是下一行代码,直到再也没有任何代码。此时 JS 虚拟机才会问:“队列里都有谁啊?”
如果队列中至少有一个事件适合于 “触发”, 则JS虚拟机会挑选一个事件,并调用此事件的处理器。事件处理器返回后,我们又回到队列中。
输入事件的工作方式完全一样:用户单击一个已附加有单击时间处理器的 DOM 元素时,会有一个单击事件排入队列。但是,该单击事件处理器要等到当前所有正在运行的代码均已结束后才会执行。
事件循环是用来描述队列工作方式的,所谓事件循环就像代码从一个循环中不断取出而运行一样:
runYourScript();
while(atLeastOneEventIsQueued){
fireNextQueuedEvent();
}
// 这隐喻着触发的每个事件都会位于堆栈轨迹的底部
事件的易调度性是JS语言最大的特色之一,像 setTimeout 这样异步函数只是简单地做延迟执行,而不是孵化新的线程。JS代码永远不会被中断,这是因为代码在运行期间只需要排队事件即可,而这些事件在代码运行结束之前不会被触发。
异步函数的类型
每一种JS环境都有自己的异步函数集,JS环境提供的异步函数通常分为两大类:I/O函数、计时器函数。如果想要应用中定义复杂的异步行为,就要使用者两类异步函数作为基本的构造块。
异步的I/O函数
Ryan Dahl创造Node.js并不是为了人们能在服务端上运行JS,而是他想要一个建立在某高级语言之上的事件驱动型服务器框架。JS碰巧就是适合干这个的语言。为什么?因为JS可以完美地实现非堵塞式I/O。
有些I/O函数既有同步效应也有异步效应,例如在现代浏览器中操纵DOM对象时,从脚本角度看更改是即时生效的,但从视效角度看,在返回事件队列之前不会渲染这些DOM对象更改。这可以防止DOM对象被渲染成不一致的状态。
异步的计时器函数
异步函数非常适合用于I/O操作,但有时候我们仅仅是因为需要异步而想要异步性。基于时间的事件涉及两个著名的函数:setTimeout、setInterval。遗憾的是这两个著名的计时器函数都有自己的缺陷,其中有个缺陷是无法弥补的:当同一个JS进程正运行着代码时,任何JS计时器函数都无法使其他代码运行起来。
var fireCount = 0;
var start = new Date;
var timer = setInterval(function(){
if(new Date - start > 1000){
clearInterval(timer);
console.log(fireCount);
return;
}
fireCount++;
}, 0);
如果使用setInterval调度事件且延迟设定为0毫秒,则会尽可能频繁地运行此事件,对吗?那么,在运行于高速Intel i7 处理器之上的现代浏览器中,此事件的触发频率到底如何呢?最后我们发现,setTimeout和setInterval就是想设计成慢吞吞的!事实上,HTML规范推行的 延迟/时隔的最小值就是4毫秒!
那么,如果需要更细粒度的计时,该怎么办呢?
异步函数的编写
JS中每个异步函数都构建在其他某个或某些异步函数之上。凡是异步函数,从上到下一直到原生代码都是异步的。反之亦然:任何函数只要使用了异步的函数,就必须以异步的方式给出其操作结果。JS并没有提供一种机制以阻止函数在异步操作结束之前返回。事实上,除非函数返回,否则不会触发任何异步事件。