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

《编写高质量代码:改善JavaScript程序的188个建议》建议181:使用setTimeout实现工作线程

关灯直达底部

在Ajax应用中,有时候需要在后台执行一些耗时较长且与页面主要逻辑无关的操作。例如,对于一个在线文档编辑器来说,会需要定期自动备份用户当前所编辑的内容,这样当应用异常崩溃时,用户还能恢复所编辑的内容。这样的定期备份任务可能需要花费一些时间,但优先级较低。类似这样的任务还有页面内容的预先加载和日志记录等。对于这些任务,最好的实现方式是在后台工作线程中执行它们,这样不会对用户在主页面上的操作造成影响。用户并不会希望由于后台备份正在进行,而无法对当前的文档进行编辑。

浏览器中JavaScript引擎是单线程执行的,也就是说,在同一时间内,只能有一段代码被JavaScript引擎执行。如果在同一时间内还有其他代码需要执行,那么这些代码需要等待JavaScript引擎执行完当前的代码之后才有可能获得被执行的机会。在正常情况下,作为页面加载过程中的重要一步,JavaScript引擎会顺序执行页面上的所有JavaScript代码。在页面加载完成之后,JavaScript引擎会进入空闲状态。用户在页面上的操作会触发一些事件,这些事件的处理方法会交给JavaScript引擎来执行。鉴于JavaScript引擎的单线程特性,一般会在内部维护一个待处理的事件队列,每次从事件队列中选出一个事件处理方法来执行。如果在执行过程中,有新的事件发生,那么新事件的处理方法只会被加入到队列中等待执行。如果当前正在执行的事件处理方法非常耗时,那么队列中的其他事件处理方法可能长时间无法得到执行,造成用户界面失去响应,严重影响用户的使用体验。

JavaScript引擎的这种工作方式类似于早期的单核CPU的调度方式。单核CPU虽然也支持多任务同时运行,但实际上同一时间只能有一个任务在执行。CPU通过时间片的轮转来保证每个任务都有一定的执行时间。JavaScript并没有原生提供与操作系统中的线程类似的结构,但可以通过定时器机制来模拟。JavaScript提供了两个基本的方法来执行与定时相关的操作,分别是setTimeout和setInterval。

❑setTimeout用来设置在指定的间隔时间之后执行某个JavaScript方法。setTimeout的方法声明非常简单:setTimout(func,time),其中参数func表示的是要执行的JavaScript方法,可以是JavaScript方法对象或方法体的字符串;参数time表示的是以毫秒为单位的间隔时间。

❑setInterval用来设置根据指定的间隔重复执行某个JavaScript方法。setInterval的方法声明与setTimeout相同:setInterval(func,time),这里参数time指定的是方法func重复执行的间隔。当setTimeout或setInterval被调用的时候,浏览器会根据设置的时间间隔来触发相应的事件。

如果代码的调用方式是setTimeout(func,100),那么该代码被执行100 ms后,定时器的事件被触发。如果这个时候JavaScript引擎中没有正在执行的其他代码,那么与此定时器对应的JavaScript方法func就可以被执行,否则,该JavaScript方法的执行就被加入到等待的队列中。当JavaScript引擎空闲的时候,会从这个队列中选择一个等待的JavaScript方法来执行。也就是说,虽然在调用setTimeout时设置的间隔时间是100 ms,但与之对应的JavaScript方法实际被执行的间隔有可能大于设定的100 ms,这取决于是否有其他代码正在被执行和执行所花费的时间。因此,setTimeout实际生效的间隔时间可能大于设定的时间。

setInterval的执行方式与setTimeout有很大不同。如果代码的调用方式是setInterval(func,100),那么每隔100 ms,定时器的事件就会被触发。与setTimeout相同的是,如果当前JavaScript引擎空闲,那么定时器对应的方法func会被立即执行,否则,该JavaScript方法的执行就会被加入到等待队列中。由于定时器的事件是每隔100 ms就触发一次,有可能某一次事件触发的时候,上一次事件的处理方法还没有机会执行,仍然在等待队列中。这个时候,这个新的定时器事件就被丢弃。需要注意的是,由于JavaScript引擎的这种执行方式,两次执行定时器事件处理方法的实际时间间隔小于设定的时间间隔。例如,在上一个定时器事件的处理方法触发之后,等待了50 ms才获得被执行的机会,而第二个定时器事件的处理方法被触发后马上就被执行了。也就是说,这两者之间的时间间隔实际上只有50 ms。因此,setInterval并不适合实现精确的按固定间隔的调度操作。

总的来说,使用setTimeout和setInterval都不能满足精确的时间间隔。通过setTimeout设置的JavaScript方法的实际执行间隔不小于设定的时间,而通过setInterval设置的重复执行的JavaScript方法的间隔可能会小于设定的时间。

setTimeout可用于设置在某个时间间隔之后再执行某个JavaScript方法。setTimeout的另外一个作用是可以将某些操作推迟执行,让出JavaScript引擎来处理其等待队列中的其他事件,以提高用户体验。例如,某个操作需要进行大量计算,平均耗时在3 s左右,当这个操作开始执行之后,就会一直占用JavaScript引擎,直到执行结束。在这个过程中,其他的JavaScript方法就被放置到JavaScript引擎的等待队列中。如果用户在这个过程中单击了页面上的某个按钮,那么相应的事件处理方法并不能马上执行,给用户的感觉就是整个Web应用暂时失去了响应,给用户带来不好的用户体验。

如果使用setTimeout,就可以把一个需要较长执行时间的任务分成若干个小任务,这些小任务之间用setTimeout串联起来。在这些小任务的执行间隔中,就可以给其他正在等待的JavaScript方法以执行机会。

这里以计算100 000以内的质数的个数为例进行介绍。求质数的方法有不少,这里使用一种简单的方法。对于每一个正整数,通过判断其是否能被小于或等于其平方根的整数整除,就可以确定其是否为质数。实现的代码如下:


function isPrime(n){

if(n==0||n==1){

return false;

}

var bound=Math.floor(Math.sqrt(n));

for(var i=2;i<=bound;i++){

if(n%i==0){

return false;

}

}

return true;

}


如果使用一般的计算方式,那么只需要通过一个很大的循环对范围之内的每个整数都进行判断即可。


function calculateNormal{

var count=0;

for(var i=2;i<=MAX;i++){

if(isPrime(i)){

count++;

}

}

alert(/"计算完成,质数个数为:/"+count);

}


上面代码的问题在于:在整个计算过程中,JavaScript引擎被全部占用,整个页面无法响应用户的其他请求,页面会呈现“假死”的状态。而通过setTimeout可以把计算任务分成若干个小任务来执行,提高页面的响应能力。实现的代码如下:


function calculateUsingTimeout{

var jobs=10,numberPerJob=Math.ceil(MAX/jobs);

var count=0;

function calculate(start,end){

for(var i=start;i<=end;i++){

if(isPrime(i)){

count++;

}

}

}

var start,end,timeout,finished=0;

function manage{

if(finished==jobs){

window.clearTimeout(timeout);

alert(/"计算完成,质数个数为:/"+count);

}

else{

start=finished*numberPerJob+1,

end=Math.min((finished+1)*numberPerJob,MAX);

timeout=window.setTimeout(function{

calculate(start,end);

finished++;

manage;

},100);

}

}

manage;

}


通过setTimeout把耗时较长的计算任务分成了10个小任务,每个任务之间的执行间隔是100 ms。在这些小任务的执行间隔中,JavaScript引擎是可以处理其他事件的,这样就保证了对用户的响应时间。虽然整个任务总的执行时间变长了,但是带来了用户体验的提升。