首页 » 编写高质量代码:改善JavaScript程序的188个建议 » 编写高质量代码:改善JavaScript程序的188个建议全文在线阅读

《编写高质量代码:改善JavaScript程序的188个建议》建议119:使用定时器优化UI队列

关灯直达底部

在JavaScript中使用setTimeout或setInterval创建定时器时,这两个函数都接收一样的参数:一个是要执行的函数,另一个是执行这个函数之前的等待时间(单位毫秒)。setTimeout函数创建一个只运行一次的定时器,而setInterval函数创建一个周期性重复运行的定时器。

定时器与UI线程交互的方式有助于分解长运行脚本为较短的片断。调用setTimeout或setInterval告诉JavaScript引擎等待一定时间,然后将JavaScript任务添加到UI队列中。例如:


function greeting{

alert(/"Hello world!/");

}

setTimeout(greeting,250);


在上面代码中,在250 ms之后向UI队列插入一个JavaScript任务来运行greeting函数。在此时间点之前,所有其他UI更新和JavaScript任务都在运行。记住,第二个参数指出什么时候应当将任务添加到UI队列之中,并不是说那时代码将被执行,这个任务必须等到队列中的其他任务都执行之后才能被执行。例如:


var button=document.getElementById(/"my-button/");

button.onclick=function{

oneMethod;

setTimeout(function{

document.getElementById(/"notice/").style.color=/"red/";

},250);

};


在上面示例中,当按钮被单击时,将调用一个方法设置一个定时器。用于修改notice元素颜色的代码被包含在一个定时器设备中,它将在250 ms之后被添加到队列中。250 ms是从调用setTimeout时开始计算的,而不是从整个函数运行结束时开始计算的。如果setTimeout在时间点n上被调用,那么运行定时器代码的JavaScript任务将在n+250的时刻加入UI队列。

定时器代码只有等创建它的函数运行完成之后才有可能被执行。假设在前面的代码中定时器延时变得更小,在创建定时器之后又调用了另一个函数,那么定时器代码有可能在onclick事件处理完成之前加入队列。


var button=document.getElementById(/"my-button/");

button.onclick=function{

oneMethod;

setTimeout(function{

document.getElementById(/"notice/").style.color=/"red/";

},50);

anotherMethod;

};


如果anotherMethod执行时间超过50 ms,那么定时器代码将在onclick处理完成之前加入到队列中。其结果是等onclick处理运行完毕,定时器代码立即执行,察觉不出其间的延迟。

在任何一种情况下,创建一个定时器会造成UI线程暂停,如同定时器会从一个任务切换到下一个任务。因此,定时器代码复位所有相关的浏览器限制,包括长运行脚本时间。此外,调用栈也在定时器代码中复位为零。这一特性使定时器成为长运行JavaScript代码理想的跨浏览器解决方案。

JavaScript定时器延时往往不准确,快慢大约几毫秒。指定定时器延时250 ms,并不意味任务将在调用setTimeout之后精确的250 ms后加入队列。所有浏览器试图尽可能准确,但通常会发生几毫秒的滑移,或快或慢。正因为这个原因,定时器不可用于测量实际时间。

在Windows系统上定时器的分辨率为15 ms,也就是说,一个值为15的定时器延时将根据最后一次系统时间的刷新而转换为0或15。由于设置定时器延时小于15将在IE中导致浏览器锁定,所以建议最小值为25 ms(实际时间是15 ms或30 ms),以确保至少15 ms的延迟。

最小定时器延时也有助于避免其他浏览器和操作系统上产生的定时器分辨率问题。大多数浏览器在定时器延时小于10 ms时表现出差异性。

一个常见的长运行脚本就是循环占用了太长的运行时间。如果尝试循环优化之后还不能缩减足够的运行时间,那么定时器就是下一个优化步骤。基本方法是将循环工作分解到定时器序列中。典型的循环模式如下:


for(var i=0,len=items.length;i<len;i++){

process(items[i]);

}


导致循环结构运行时间过长的因素有两个:process的复杂度和items的大小。这两个因素有可能同时存在。可用定时器取代循环的两个决定性因素如下:

❑处理过程不需要同步处理。

❑数据不需要按顺序处理。

一种基本异步代码模式如下:


var todo=items.concat;

setTimeout(function{

process(todo.shift);

if(todo.length>0){

setTimeout(arguments.callee,25);

}else{

callback(items);

}

},25);


这个模式的基本思想是创建一个原始数组的副本,将它作为处理对象。第一次调用setTimeout创建一个定时器处理队列中的第一个项。调用todo.shift返回它的第一个项,然后将它从数组中删除。第一项的值作为参数传给process。接着检查是否还有更多项需要处理。如果todo队列中还有内容,那么就再启动一个定时器。因为下个定时器需要运行相同的代码,所以将第一个参数传入arguments.callee,此值指向当前正在运行的匿名函数。如果不再有内容需要处理,那么将调用callback函数。此模式与循环相比需要更多代码,可将此功能封装起来,例如:


function processArray(items,process,callback){

var todo=items.concat;

setTimeout(function{

process(todo.shift);

if(todo.length>0){

setTimeout(arguments.callee,25);

}else{

callback(items);

}

},25);

}


processArray函数以一种可重用的方式实现了先前的模板,并且接收3个参数:待处理数组、对每个项调用的处理函数、处理结束时执行的回调函数。该函数用法如下:


var items=[123,789,323,778,232,654,219,543,321,160];

function outputValue(value){

console.log(value);

}

processArray(items,outputValue,function{

console.log(/"Done!/");

});


此段代码使用processArray方法将数组值输出到终端,当所有处理结束时再打印一条消息。通过将代码封装在一个函数中,定时器可在多处重用而无须多次实现。