setTimeout(arguments.callee,10);
}else{
onComplete();
}
})();
}
这个函数借助一个异步管理器来实现了冒泡算法,在每次遍历数组以前暂停一下。
onComplete()函数会在数组排序完成后触发,提示用户数据已经准备好。
bubbleSort()函数使用了和chunk()函数一样的基本技术(参考我的上一篇帖子),将行为包装在一个匿名函数中,将arguments.callee传递给setTimeout()以达到重复操作的目的,直至排序完成。
如果你要将嵌套的循环拆解成若干个小步骤,以达到解放浏览器的目的,这个函数提供了不错的指导意见。
相似的问题还包括过多的递归。
每个额外的递归调用都会占用更多的内存,从而减慢浏览器的运行。
恼人的是,你可能在浏览器发出脚本失控警告之前,就耗尽了系统的内存,导致浏览器处于停止响应的状态。
Crockford在博客上曾经对这个问题进行过深入的讨论。
他当时使用的例子,就是用递归生成一个斐波那契数列。
functionfibonacci(n){
returnn<2?
n:
fibonacci(n-1)+fibonacci(n-2);
};
按照Crockford的说法,执行fibonacci(40)这条语句将重复调用自身331160280次。
避免使用递归的方案之一就是使用memoization技术,这项技术可以获取上一次调用的执行结果。
Crockford介绍了下面这个函数,可以为处理数值的函数增加这项功能:
functionmemoizer(memo,fundamental){
varshell=function(n){
varresult=memo[n];
if(typeofresult!
=='number'){
result=fundamental(shell,n);
memo[n]=result;
}
returnresult;
};
returnshell;
};
他接下来将这个函数应用在斐波那契数列生成器上:
varfibonacci=memoizer([0,1],
function(recur,n){
returnrecur(n-1)+recur(n-2);
});
这时如果我们再次调用fibonacci(40),只会重复调用40次,和原来相比提高得非常多。
memoization的原理,概括起来就一句话,同样的结果,你没有必要计算两次。
如果一个结果你可能会再次使用,把这个结果保存起来,总比重新计算一次来的快。
最后一个可能让函数执行缓慢的原因,就是我们之前提到过的,函数里面执行了太多的内容,通常是因为使用了类似下面的开发模式:
functiondoAlot(){
doSomething();
doSomethingElse();
doOneMoreThing();
}
在这里要执行三个不同的函数,请注意,无论是哪个函数,在执行过程中都不依赖其他的函数,他们在本质是相对独立的,只是需要在一个特定时间逐一执行而已。
同样,你可以使用类似chunk()的方法来执行一系列函数,而不会导致锁定浏览器。
functionschedule(functions,context){
setTimeout(function(){
varprocess=functions.shift();
process.call(context);
if(functions.length>0){
setTimeout(arguments.callee,100);
}
},
100);
}
schedule函数有两个参数,一个是包含要执行函数的数组,另外一个是标明this所属的上下文对象。
函数数组以队列方式实现,Timer事件每次触发的时候,都会将队列最前面的函数取出并执行,这个函数可以通过下面的方式执行一系列函数:
schedule([doSomething,doSomethingElse,doOneMoreThing],window);
很希望各个JavaScript的类库都增加类似这样的进程处理函数。
YUI在3.0时就已经引入了Queue对象,可以通过timer连续调用一组函数。
无论现有的技术可以帮助我们将复杂的进程拆分到什么程度,对于开发者来说,使用这种方法来理解并确定脚本失控的瓶颈是非常重要的。
无论是太多的循环、递归还是其他的什么,你现在应该知道如果处理类似的情况。
但要记住,这里提到的技术和函数只是起到抛砖引玉的作用,在实际的应用中,你应该对它们加以改进,这样才能发挥更大的作用。
递归是拖慢脚本运行速度的大敌之一。
太多的递归会让浏览器变得越来越慢直到死掉或者莫名其妙的突然自动退出,所以我们一定要解决在JavaScript中出现的这一系列性能问题。
在这个系列文章的第二篇中,我曾经简短的介绍了如何通过memoization技术来替代函数中太多的递归调用。
memoization是一种可以缓存之前运算结果的技术,这样我们就不需要重新计算那些已经计算过的结果。
对于通过递归来进行计算的函数,memoization简直是太有用了。
我现在使用的memoizer是由Crockford写的,主要应用在那些返回整数的递归运算中。
当然并不是所有的递归函数都返回整数,所以我们需要一个更加通用的memoizer()函数来处理更多类型的递归函数。
functionmemoizer(fundamental,cache){
cache=cache||{};
varshell=function(arg){
if(!
(argincache)){
cache[arg]=fundamental(shell,arg);
}
returncache[arg];
};
returnshell;
}
这个版本的函数和Crockford写的版本有一点点不同。
首先,参数的顺序被颠倒了,原有函数被设置为第一个参数,第二个参数是缓存对象,为可选参数,因为并不是所有的递归函数都包含初始信息。
在函数内部,我将缓存对象的类型从数组转换为对象,这样这个版本就可以适应那些不是返回整数的递归函数。
在shell函数里,我使用了in操作符来判断参数是否已经包含在缓存里。
这种写法比测试类型不是undefined更加安全,因为undefined是一个有效的返回值。
我们还是用之前提到的斐波纳契数列来做说明:
varfibonacci=memoizer(function(recur,n){
returnrecur(n-1)+recur(n-2);
},{"0":
0,"1":
1});
同样的,执行fibonacci(40)这个函数,只会对原有的函数调用40次,而不是夸张的331,160,280次。
memoization对于那些有着严格定义的结果集的递归算法来说,简直是棒极了。
然而,确实还有很多递归算法不适合使用memoization方法来进行优化。
我在学校时的一位教授一直坚持认为,任何使用递归的情况,如果有需要,都可以使用迭代来代替。
实际上,递归和迭代经常会被作为互相弥补的方法,尤其是在另外一种出问题的情况下。
将递归算法转换为迭代算法的技术,也是和开发语言无关的。
这对JavaScript来说是很重要的,因为很多东西在执行环境中是受到限制的(theimportanceinJavaScriptisgreater,though,becausetheresourcesoftheexecutionenvironmentaresorestrictive.)。
让我们回顾一个典型的递归算法,比如说归并排序,在JavaScript中实现这个算法需要下面的代码:
functionmerge(left,right){
varresult=[];
while(left.length>0&&right.length>0){
if(left[0]result.push(left.shift());
}else{
result.push(right.shift());
}
}
returnresult.concat(left).concat(right);
}
//采用递归实现的归并排序算法
functionmergeSort(items){
if(items.length==1){
returnitems;
}
varmiddle=Math.floor(items.length/2),
left=items.slice(0,middle),
right=items.slice(middle);
returnmerge(mergeSort(left),mergeSort(right));
}
调用mergeSort()函数处理一个数组,就可以返回经过排序的数组。
注意每次调用mergeSort()函数,都会有两次递归调用。
这个算法不可以使用memoization来进行优化,因为每个结果都只计算并使用一次,就算缓冲了结果也没有什么用。
如果你使用mergeSort()函数来处理一个包含100个元素的数组,总共会有199次调用。
1000个元素的数组将会执行1999次调用。
在这种情况下,我们的解决方案是将递归算法转换为迭代算法,也就是说要引入一些循环(关于算法,可以参考这篇《ListProcessing:
SortAgain,Naturally》):
//采用迭代实现的归并排序算法
functionmergeSort(items){
if(items.length==1){
returnitems;
}
varwork=[];
for(vari=0,
len=items.length;iwork.push([items[i]]);
}
work.push([]);//incaseofoddnumberofitems
for(varlim=len;lim>1;lim=(lim+1)/2){
for(varj=0,
k=0;kwork[j]=merge(work[k],work[k+1]);
}
work[j]=[];//incaseofoddnumberofitems
}
returnwork[0];
}
这个归并排序算法实现使用了一系列循环来代替递归进行排序。
由于归并排序首先要将数组拆分成若干只有一个元素的数组,这个方法更加明确的执行了这个操作,而不是通过递归函数隐晦的完成。
work数组被初始化为包含一堆只有一个元素数组的数组。
在循环中每次会合并两个数组,并将合并后的结果放回work数组中。
当函数执行完成后,排序的结果会通过work数组中的第一个元素返回。
在这个归并排序的实现中,没有使用任何递归,同样也实现了这个算法。
然而,这样做却引入了大量的循环,循环的次数基于要排序的数组中元素的个数,所以我们可能需要使用在上篇讨论过的技术来进行修订,处理这些额外开销。
总结一下基本原则,不管是什么时候使用递归的时候都应该小心谨慎。
memoization和迭代是代替递归的两种解决方案,最直接的结果当然就是避免那个提示脚本失控的对话框。
Web开发者经常遇到并必须及时处理的问题就是“提示脚本运行时间过长的提示框”(或者称为“失控脚本提示”),这些令人讨厌的对话框会在你的脚本执行时间过长的时候出现。
对于Web开发者的基本准则就是,无论什么时候,都不要让用户看到这些对话框,因为这会给人一种代码缺乏结构化的印象,更简单的说,你的代码负担太重了。
用BrendanEich(JavaScript的发明人)的话来讲,如果JavaScript运行的时间需要用秒来计算,一定是什么地方搞错了。
我个人可以忍受的上限可能更小一些,不论什么脚本,在任何时间、任何浏览器上执行,都不应该超过100毫秒。
如果实际执行的时间长于这个底限,一定要将进程分解成若干更小的代码段。
另外,其实很少有人真正意识到究竟是什么原因导致脚本在不同的浏览器中运行时间过长,连我自己都没有深究过。
所以我决定坐下来好好研究一下,我们究竟会在什么情况才会看到那个讨厌的对话框。
判断脚本是否失控,无外乎就两种方法。
一种是根据执行了多少条语句,一种是判断脚本执行花费的时间。
各个浏览器判断脚本失控的具体方法会有略微的不同。
InternetExplorer
InternetExplorer判断一个脚本是否失控,主要通过JScript引擎执行语句的总数来判断。
默认情况下,这个上限是500万条语句,这个值是可以通过注册表修改的。
当你的脚本执行的语句数量超过这个限制,你就会看到下面的窗口。
这个对话框提示:
“这个页面上有一段脚本导致InternetExplorer运行缓慢,如果你继续运行,你的计算机可能会变为无响应状态”。
要不是追求技术上的准确性,这样说确实有点过了。
对话框有两个选项,要么停止脚本执行,要么允许脚本继续运行。
当这个对话框显示的时候,脚本已经被完全停止了。
如果你选择继续运行脚本,就会重新计算当前执行的语句数,也就是说,如果这个数值再次达到上限时,你会再次看到这个对话框。
Firefox
Firefox是根据脚本引擎持续执行代码的时间来判断一段脚本是否失控。
默认的上限是10秒,可以通过about:
config页面来修改这个值。
这里需要注意的是,当弹出类似alert的模式对话框的时候,是不计时的。
当浏览器执行脚本的时间达到这个上限,Firefox就会显示类似下面的对话框:
Firefox的对话框提示:
“这个页面的一段脚本目前运行忙,或者这段脚本已经停止响应。
你可以停止执行这段脚本,并在调试器中打开这段脚本,或者保持这段脚本继续运行”。
更清楚的描述了遇到的问题,并且没有IE说的那么恐怖。
在这个对话框上可以执行三种操作:
停止脚本执行、调试脚本或者让脚本继续运行。
和InternetExplorer一样,当运行脚本继续运行以后,对持续运行脚本时间的统计就会重置。
调试脚本按钮,只有在你安装了Firebug,并在该页面激活了调试的时候才会出现。
执行调试脚本操作后,可以显示执行时间过长的代码段的具体位置。
Safari
Safari同样根据脚本引擎持续执行脚本的时间来判断,当我对Webkit的源代码进行反复研究后,发现默认的超时时间是5秒,一旦达到这个上限,就会给出下面的对话框提示:
对话框提示:
“在页面url上的脚本让Safari失去响应,你是要继续运行脚本还是终止脚本”。
同样的,对于用户来说,也不是什么可怕的提示。
在Safari中,可以关闭失控脚本的检测功能。
Chrome
Chrome在跟踪技术上有点狡猾,失控脚本检测功能似乎和tab的事故控制(crashcontrol)关联到一起。
我仔细看了源代码,却没有找到具体的限制,但基本确定的是,这个限制是以时间为基础的,估计在10秒左右(要么是5秒,要么是10秒,总要和Safari或者Firefox看齐么)。
我正在联系Chrome项目组中的朋友,看看能不能得到确定的信息。
尽管如此,如果网页中存在失控的脚本,用户还是会看到下面的对话框:
毫无疑问,Chrome