1、昂贵计算的处理
在复杂javascript应用程序开发中,最复杂的可能是用户界面的单线程特性。而javascript在处理用户交互时最好的状况是反应迟钝,最糟糕的情况是无响应而导致浏览器挂起(在javascript执行时,页面中所有的更新操作暂停)。源于这一事实,将所有复杂操作(任何多于100ms的计算)减小到可管理的程度就势在必行。另外,如果运行了至少5秒钟还没有停止,一些浏览器(如firefox 、opera)将产生一个提示框警告用户脚本无相应。
这显然是不可取的,产生一个无响应的界面并不好。但是,几乎可以肯定的是,当你需要处理大量数据时就会出现这种情况(如处理数以千计的dom元素会导致这种情况出现)。
此时,计时器就显得尤为有用。由于计时器能有效的暂停javascript代码的执行,它也能阻止浏览器将执行的代码挂起(只要个别代码还不足以使浏览器挂起)。想到这一点,我们可以将正常的、密集的、循环计算纳入到非阻塞的计算之中,让我们看看下面这个例子,这种类型的计算是必需的。
一个长时运行的任务:
<table><tbody></tbody></table>
// normal, intensive, operation
var table = document.getelementsbytagname("tbody");
for ( var i = 0; i < 2000; i++ ) {
var tr = document.createelement("tr");
for ( var t = 0; t < 6; t++ ){
var td = document.createelement("td");
td.appendchild( document.createtextnode("" + t));
tr.appendchild( td );
}
table.appendchild( tr );
}
}
在这个例子中,我们创建了总数为26000个dom节点,并将数字填入表中,这太昂贵了,很有可能将浏览器挂起以阻止用户正常的交互。我们可以将计时器引入其中,从而得到与众不同,也许更好的结果。
用计时器将耗时较长的任务拆分开来:
<table><tbody></tbody></table>
var table = document.getelementsbytagname("tbody");
var i = 0, max = 1999;
settimeout(function(){
for ( var step = i + 500; i < step; i++ ) {
var tr = document.createelement("tr");
for ( var t = 0; t < 6; t++ ){
var td = document.createelement("td");
td.appendchild( document.createtextnode("" + t));
tr.appendchild( td );
}
}
table.appendchild( tr );
}
if ( i < max )
settimeout( arguments.callee, 0 );
}, 0);
在我们修改的例子中,我们将密集的计算分成四部分,每个创建6500个节点。这些计算不太可能中断浏览器正常的流。最糟糕的情况也只是这些数字可能随时调整(例如,使其在250-500之间变化,这样我们的每一个单元将产生3500dom个节点)。但是,给人印象最深的是我们如何改变我们的代码以适应新的异步系统。我们需要做更多的工作以确保元素的数字正确生成(该循环不会永无休止)。这些代码与我们初始的很相似。注意,我们使用闭包维持代码片段间的迭代状态,毫无疑问,不使用闭包,此代码将更为复杂。
与原来的技术相比,使用该技术有一个明显的变化。浏览器的长时挂起被4个视觉化的页面更新替代。虽然浏览器尝试着尽可能快得去执行这些代码片段,它也在计时器的每一步操作之后渲染dom变化(就好像大量的更新)。大多数情况下,用户觉察不到此种类型的更新,但记住它们曾经发生过很重要。
有一种情况会使该技术能专门为我服务--我构建的计算大学生日程排列的应用程序。起初,该应用程序是典型的cgi(客户与服务器交流,服务器计算出日程表之后将其返回)。但我对它作了改变,讲所有的日程计算放到客户端,这是日程计算的视图:
这些计算的代价相当昂贵(需要遍历数以千计的排列以找到正确的答案)。将日程计算分割成实实在在的单元使这一问题得到了解决(用已经完成的那部分更新用户界面)。最后,用户提交的是快速、反应灵敏、可用性较高的用户界面。
如此有用的技术常常令人惊奇。你会发现它经常被用于长时运行的程序之中,就像测试箱(我们在这章末尾讨论它)。更为重要的是,这种技术向我们显示了解决浏览器环境的限制是多么的轻而易举,同时也为用户提供了丰富的经验。
|||
2、中央计时器控件
在使用计时器时,出现了一个问题--在处理大量计时器时如何管理它们。在动画处理中你尝试去同步处理大量属性时就尤其重要,你需要一种方法去管理它们。
你的计时器有一个核心的控制将赋予你很大的权力和灵活性,即:
你也必需认识到,增加同步计时器的数量有可能增加浏览器垃圾回收出现的可能性。一般来说,浏览器在搜寻并尝试绑定任何零碎的东西(删除未使用的变量、对象等)。因为它们通常在正常的javascript引擎管理之外(通过其它线程),这时定时器的问题就尤为严重。一些浏览器能处理这种情况而另外一些浏览器会导致长时的垃圾回收循环出现。你也许会注意到这种问题--当你在一个浏览器中看到的是漂亮、平滑的动画,而在另一个浏览器中动画是走走停停完成的。减少计时器同步应用的数量对此大有裨益(这也是现代浏览器引入类似中央计时器控件构件的原因)。
让我们来看一个例子,我们有多个函数,这些函数分别对单个属性产生动画效果,但它们被一个单独的计时器函数管理。
用计时器队列控制多重动画:
<div id="box" style="position:absolute;">hello!</div>
var timers = {
timerid: 0,
timers: [],
start: function(){
if ( this.timerid )
return;
(function(){
for ( var i = 0; i < timers.timers.length; i++ )
if ( timers.timers[i]() === false ) {
timers.timers.splice(i, 1);
i--;
}
timers.timerid = settimeout( arguments.callee, 0 );
})();
},
stop: function(){
cleartimeout( this.timerid );
this.timerid = 0;
},
add: function(fn){
this.timers.push( fn );
this.start();
}
};
var box = document.getelementsbyid("box"), left = 0, top = 20;
timers.add(function(){
box.style.left = left + "px";
if ( ++left > 50 )
return false;
});
timers.add(function(){
box.style.top = top + "px";
top += 2;
if ( top > 120 )
return false;
});
在这我们创建了一个中央控制结构。我们可以添加新的计时器回调函数,并可停止和开始它们的执行。此外,回调函数有能力在任何时候删除自己,只需简单的返回“false”即可(这比通常的cleartimeout模式更容易),让我们逐一分析代码看看它是如何工作的。
起初,所有回调函数连同当前计时器的id(timers.id)被存储在一个中央数组中(timers.timers)。核心内容在start()函数内部,在这里我们需要确认得是已没有计时器在运行,如果是那样,就开始我们的中央计时器。
计时器包含一个循环,它遍历所有的回调函数,并按顺序执行它们,它还检查回调函数返回的值,如果是“false”,将从执行中移除。事实证明,这是计时器管理非常简单的方式。
有一点非常重要,用这种方式组织计时器会确保回调函数总是按顺序执行,那样,正常的计时器总是得不到保证(浏览器一个接一个的选择执行)。
定时器的这种组织方式对于大型应用程序或任何形式的真正的javascript动画至关重要,当我们讨论如何创建跨浏览器动画时,有一种解决方案肯定有助于将来任何形式的应用开发。
|||
3、异步测试
另外一种中央计时器控件能派上用场的情况是在你打算做异步测试的时候。这里的问题是当我们需要对没有立即完成的计算执行测试时(如计时器内的一些行为或xmlhttprequest)。我们需要将测试包分解,这样就会完全异步工作。
例如,在一个正常的测试包中,我们可以很容易的运行这些测试。但是,一旦在我们的测试中引入这种需求,我们需要分解它们并单独处理。我们可以用下面的代码达到我们期望的效果。
简单的异步测试包:
(function(){
var queue = [], timer;
this.test = function(fn){
queue.push( fn );
resume();
};
this.pause = function(){
clearinterval( timer );
timer = 0;
};
this.resume = function(){
if ( !timer )
return;
timer = setinterval(function(){
if ( queue.length )
queue.shift()();
else
pause();
}, 1);
};
})();
test(function(){
pause();
settimeout(function(){
assert( true, "first test completed" );
resume();
}, 100);
});
test(function(){
pause();
settimeout(function(){
assert( true, "second test completed" );
resume();
}, 200);
});
最重要的一个方面是,传递给test()的每一个函数至多包含一个异步测试。其异步性是通过使用pause()和resume()函数来定义的,这些函数在异步事件前后调用。实际上,上面的代码只不过是保持异步行为的一种手段--其包含的函数以特定的顺序执行(它不完全用于本测试包,但在这非常有用)。
让我们看看使这种行为成为可能的代码。函数的大部分功能包含在resume()函数中,它的行为与我们前面例子中的start()方法相似,但它主要用来处理队列数据,其主要目的是取出一个函数并执行它。如果有一个在等待,它将完全停止运行。最重要的方面是,既然队列处理代码完全是异步的,他就能保证在我们调用pause()函数之后尝试执行。
这一段简短的代码使我们的测试以完全异步的方式执行,但仍维持着test()函数执行的顺序(如果结果具有毁坏性且影响其它测试,这就非常关键)。谢天谢地,我们可以看到,使用最有效的计时器,给一个存在的测试包增加可靠的异步测试并不需要所有的开销。
|||
4、总结
了解javascript函数如何工作启发了我们:这些看似简单的特征在其执行时相当复杂。但是,考虑到它的复杂性,我们会更加深入的研究它们。很明显,计时器最终在复杂的应用程序中特别有用(计算昂贵的代码、动画、异步测试包)。但由于其易用性(特别是增加了闭包),它们往往易于掌握,即使是在最复杂的情况下。
附文章中提到的assert()函数:(该函数是在jquery的基础上创建的)
function assert(pass, msg){
var type = pass ? "pass" : "fail";
jquery("#results").append("<li class='" + type + "'><b>" + type + "</b> " + msg + "</li>");
}
新闻热点
疑难解答