如何提升JavaScript操作DOM的效率.docx
- 文档编号:11981142
- 上传时间:2023-04-16
- 格式:DOCX
- 页数:29
- 大小:151.43KB
如何提升JavaScript操作DOM的效率.docx
《如何提升JavaScript操作DOM的效率.docx》由会员分享,可在线阅读,更多相关《如何提升JavaScript操作DOM的效率.docx(29页珍藏版)》请在冰豆网上搜索。
如何提升JavaScript操作DOM的效率
如何提升JavaScript操作DOM的效率
Nicholas为您讲解如何提升JavaScript操作DOM的效率!
在Web开发中,JavaScript的一个很重要的作用就是对DOM进行操作,可你知道么?
对DOM的操作是非常昂贵的,因为这会导致浏览器执行回流操作,而执行了过多的回流操作,你就会发现自己的网站变得越来越慢了,我们应该尽可能的减少DOM操作。
本文是这个系列的最后一篇,给出了一些指导性原则,比如在什么时候应该对DOM可以进行什么样的操作等。
在过去的几周中,我为大家介绍了几种可以加快JavaScript脚本运行速度的技术。
脚本失控基本上有以下四个方面的原因:
1.在循环中执行了太多的操作。
2.臃肿的函数体
3.过多的递归
4.过多的DOM调用
在这篇帖子中,我将会把重点放到第一条上:
循环中的过多操作。
循环的操作是同步进行的,所以执行一个循环所花费的时间完全取决于循环的次数。
因此有两种情况会导致循环执行的时间过长,并直接导致锁定浏览器。
一是循环体中包含了太多的操作,二是循环的次数过多。
这两种情况都能直接导致锁定浏览器,并显示脚本失控的提示。
解决这个问题的诀窍就是用下面这两个问题来评估每个循环:
1.这个循环必须要同步执行么?
2.循环里面的数据,必须要按顺序执行么?
如果两个问题的答案都是否定的话,你就可以选择将循环里的操作进行分解。
关键是要根据代码的具体环境确定上面两个问题的答案。
一个典型的循环可能像下面这个样子:
for(vari=0;i process(items[i]); } 乍一看这个循环并没有太大的问题,是不是会运行很长时间完全取决于循环的次数。 如果紧接循环后没有其他代码在执行的时候需要依赖于循环的结果,那么对于第一个问题的答案就是“不”。 你还可以发现,循环每次只处理一个数值,而且不依赖于上一次循环的结果,所以对于第二个问题的答案同样也是否定的。 这就意味着,循环可以通过某种方式进行拆解,不会导致锁定浏览器而显示脚本失控的提示。 在《ProfessionalJavaScript,SecondEdition》这本书中,对于那些执行次数非常巨大的虚幻,我推荐使用下面的方式来处理: functionchunk(array,process,context){ setTimeout(function(){ varitem=array.shift(); process.call(context,item); if(array.length>0){ setTimeout(arguments.callee,100); } },100); } chunk()函数的用途就是将一个数组分成小块处理(这也是名字的由来),我们可以传递三个参数。 要处理的数组对象、处理函数以及一个可选的上下文变量,用于设置process()函数中对应的this对象。 第一个timer用于处理操作之间的延时(这里设置为100毫秒,大家可以根据实际需要自行修改)。 每次执行这个函数,都会将数组中的第一个对象取出,并传给process()函数进行操作,如果这时process()中还有未处理完的对象,另外一个timer就会启动,用于重复等待。 上面提到的循环,可以通过下面的方法使用这个函数: chunk(items,process); 需要注意的是,在这里数组采用了队列(queue)的形式,而且在循环的过程中,每次都会发生修改。 如果你要修改数组的原始状态,这里介绍两种途径: 一种是通过concat()函数,在传递之前,建立一个当前数组的副本: chunk(items.concat(),process); 另外一种选择是直接修改chunk()函数,直接在函数内部进行修改: functionchunk(array,process,context){ varitems=array.concat();//clonethearray setTimeout(function(){ varitem=items.shift(); process.call(context,item); if(items.length>0){ setTimeout(arguments.callee,100); } },100); } 注意这种方法要比只保存一个索引安全的多,因为数组的内容在下次计时器生效之前可能会发生变化。 这里提到的chunk()函数,只是优化循环性能的一个起点。 你可以根据需要不断改进它,让它拥有更多的功能。 比如说,在数组中所有对象都处理完成以后,可以增加一个函数回调。 无论你是否会按照这种方式对函数进行修改,这只是一种JavaScript的代码开发模式,可以帮助优化数组的处理性能,还可以避免那个脚本失控的警告。 太多的循环经常是以嵌套的形式出现,这种代码会一直占用JavaScript引擎直至循环结束。 这方面有一个非常著名的例子,就是使用冒泡算法排序。 由于JavaScript有内置的sort()方法,我们没有必要使用这种方式进行排序,但我们可以借助这个算法理解嵌套循环占用资源的症结所在,从而避免类似情况的发生。 下面是一个在JavaScript使用冒泡排序法的典型例子: functionbubbleSort(items){ for(vari=items.length-1;i>=0;i--){ for(varj=i;j>=0;j--){ if(items[j] vartemp=items[j]; items[j]=items[j-1]; items[j-1]=temp; } } } } 回忆一下你在学校学习的计算机知识,你可能记得冒泡排序法是效率最低的排序算法之一,原因是对于一个包含n个元素的数组,必须要进行n的平方次的循环操作。 如果数组中的元素数非常大,那么这个操作会持续很长时间。 内循环的操作很简单,只是负责比较和交换数值,导致问题的最大原因在于循环执行的次数。 这会导致浏览器运行异常,潜在的直接结果就是那个脚本失控的警告对话框。 几年前,Yahoo的研究员JulienLecomte写了一篇题为《RunningCPUIntensiveJavaScriptComputationsinaWebBrowser》的文章,在这篇文章中作者阐述了如何将很大的javaScript操作分解成若干小部分。 其中一个例子就是将冒泡排序法分解成多个步骤,每个步骤只遍历一次数组。 我对他的代码做了改进,但方法的思路还是一样的: functionbubbleSort(array,onComplete){ varpos=0;(function(){ varj,value; for(j=array.length;j>pos;j--){ if(array[j] value=data[j]; data[j]=data[j-1]; data[j-1]=value; } } pos++; if(pos 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;i work.push([items[i]]); } work.push([]);//incaseofoddnumberofitems for(varlim=len;lim>1;lim=(lim+1)/2){ for(varj=0, k=0;k work[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
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 如何 提升 JavaScript 操作 DOM 效率