JQuery each()函数如何优化循环DOM结构的性能


Posted in Javascript onDecember 10, 2012

如果对jQuery这东西只停留在用的层面,而不知其具体实现的话,真的很容易用出问题来。这也是为什么近期我一直不怎么推崇用jQuery,这框架的API设定就有误导人们走上歧途之嫌。

$.fn.beautifyTable = function(options) { 
//定义默认配置项,再用options覆盖 
return this.each(function() { 
var table = $(this), 
tbody = table.children('tbody'), 
tr = tbody.children('tr'), 
th = tbody.children('th'), 
td = tbody.children('td'); 
//单独内容的class 
table.addClass(option.tableClass); 
th.addClass(options.headerClass); //1 
td.addClass(options.cellClass); //2 
//奇偶行的class 
tbody.children('tr:even').addClass(options.evenRowClass); //3 
tbody.children('tr:odd').addClass(options.oddRowClass); //4 
//对齐方式 
tr.children('th,td').css('text-align', options.align); //5 
//添加鼠标悬浮 
tr.bind('mouseover', addActiveClass); //6 
tr.bind('mouseout', removeActiveClass); //7 
//点击变色 
tr.bind('click', toggleClickClass); //8 
}); 
};

总的来说,这段代码不错,思路清晰,逻辑明确,想要做什么也通过注释说得很明白了。但是按作者的说法,当表格中有120行时,IE已经反映脚本运行时间过长了。显然从表现来看,这个函数的效率不高,甚至说极其低下。

于是,开始从代码层面进行分析,这是一个标准的jQuery插件式的函数,有个典型的return this.each(function( ) { 。.. };);形式的代码,如果作者写下这段代码的时候,不是照本宣科不经思考的话,就应该意识到jQuery的一个函数干了什么事。

简单来说,jQuery.fn下的函数,绝大部分是一个each的调用,所谓each,自然是对选择出来的元素进行了遍历,并对某个元素进行了指定的操作。那么看看上面一段代码,进行了多少的遍历,在此就假设只选择了120行,每一行有6列,另加上1行的表头吧:
遍历th,添加headerClass,元素数为6。
遍历td,添加cellClass,元素数为6*120=720。
从所有tr中找出奇数的,需要对所有tr进行一次遍历,元素数为120。
遍历奇数的tr,添加evenRowClass,元素数为120/2=60。
从所有tr中找出偶数的,需要对所有tr进行一次遍历,元素数为120。
遍历偶数的tr,添加oddRowClass,元素数为120/2=60。
遍历所有th和td,添加text-align,元素数为120*6+6=726。
遍历所有tr,添加mouseover事件,元素数为120。
遍历所有tr,添加mouseout事件,元素数为120。
遍历所有tr,添加click事件,元素数为120。
为了方便,我们简单地假设,在遍历中访问一个元素耗时为10ms,那么这个函数一共用了多少时间呢?这个函数共遇上了2172个元素,耗时21720ms,即21秒,显然IE确实应该报脚本执行过久了。

知道了效率低下的原因,要从根本上进行解决,自然要想方设法来合并循环,初略一看,按照上边代码中注释里的数字,至少以下几点是可以合并的:
3和4可以合并为一次循环,从120+60+120+60变为120,减少了240。1、2和5可以合并为一次循环,从6+720+726变为726,减少了726。6、7、8可以合并为一次循环,从120+120+120变为120,减少了240。进一步的,3、4和6、7、8一样可以合并为一次循环,继续减少了120。累加一下,我们一共减少了240+726+240+120=1326次元素操作,总计13260ms。在优化之后,我们的函数耗时变为21720-13260=8460ms,即8s。
到这里可能会有一个疑问,从表格的结构上来说,所有的th和td元素肯定都在tr之内,那么为什么不将1、2、5这三步的循环同样放到对tr的循环中,形成一个嵌套的循环,这样不是更加快速吗?
这里之所以没有这么做,主要有2个原因:
其一,无论将1、2、5这三者放在哪里,都不会减少对所有th和td元素的一次访问。
另一方面,$(‘th,td')这个选择器,在sizzle中会被翻译成2次getElementsByTagName函数的调用,第一次获取所有th,第二次获取所有td,然后进行集合的归并。由于getElementsByTagName是内置函数,在此可以认为该函数是不带循环的,即复杂度为O(1),同样集合的归并使用Array的相关函数,是对内存的操作,复杂度同样为O(1)。

反之,如果在对tr元素的循环中再采用$(‘th,'td)这个选择器,则是在tr元素上调用2次getElementsByTagName,由于无论在哪个元素上调用该函数,函数执行的时间是相同的,因此在循环tr时使用,反而多出了119*2次的函数调用,效率不升反降。
可见,对sizzle选择器的基本知识,也是帮助优化jQuery代码的很重要的一方面。
不要啥都让javascript来做。

根据前面的基本的优化,已经将时间从21秒降到了8秒,但是8秒这个数字显然是无法接受的。
再进一步分析我们的代码,事实上,循环遍历是语言层面上的内容,其速度应该是相当快的。而针对每个元素所做的操作,是jQuery提供的函数,相比遍历来说,才是占去大部分资源的主子。如果说遍历中访问元素用时是10ms的话,不客气地说执行一个addClass至少是100ms级别的消耗。

因此,为了进一步地优化效率,就不得不从减少对元素的操作入手。再仔细地回审代码,发现这个函数有着非常多的对样式的修改,其中至少包括了:
给所有th加上class。
给所有td加上class。
给tr分奇偶行加上class。
给所有th和td加上一个text-align样式。
而事实上我们知道,CSS本身就拥有子代选择器,而浏览器原生对CSS的解析,效率远远高于让javascript去给元素一一加上class。

所以,如果对CSS是可控的,那么这个函数就不应该拥有headerClass、cellClass这两个配置项,而是尽可能地在CSS中进行配置:

.beautiful-table th { /* headerClass的内容 */ } 
.beautiful-table td { /* cellClass的内容 */ }

再者,对于tr的奇偶行样式,在部分浏览器下可以使用:nth-child伪类来实现,这方面可以利用特性探测,仅在不支持该伪类的浏览器中使用addClass添加样式。当然如果你仅仅想对IE系列进行优化的话,这一条可以忽略了。

对于:nth-child伪类的探测,可以用以下的思路来进行:创建一个stylesheet,再创建一条规则,如#test span:nth-child(odd) { display: block; }。创建相应的HTML结构,一个id为test的div,内部放置3个span。

将stylesheet和div一同加入的DOM树中。查看第1和第3个span的运行期display样式,如果是block,则表明支持该伪类。删除创建的stylesheet和div,别忘了缓存探测的结果。最后,对于给所有th和td元素添加text-align样式,也是可以通过css进行优化的。既然不知道添加的是哪个align,那么就多写几个样式:

/* CSS样式 */ 
.beautiful-table-center th,.beautiful-table-center td { text-align: center !important; } 
.beautiful-table-rightright th,.beautiful-table-rightright td { text-align: rightright !important; } 
.beautiful-table-left th,.beautiful-table-left td { text-align: left !important; } 
/* javascript */ 
table.addClass('beautiful-table-' + options.align);

当然,上面所说的优化,是建立在对CSS有控制权的情况下的,如果本身无法接触到CSS样式,比如这是一个通用的插件函数,会被完全无法控制的第三方使用,那么怎么办呢?也不是完全没有办法:
去找页面里的所有CSS规则,比如document.styleSheets。遍历所有规则,把配置项中的headerClass、cellClass等拿出来。提取需要的几个class中的所有样式,再自己组装成新的选择器,如beautiful-table th。使用创建出来的选择器,生成新的stylesheet,加入到DOM树中。那么只给table加上beautiful-table这个class就搞定了。

当然上面的做法其实也蛮消耗时间的,毕竟又要遍历stylesheet,又要创建stylesheet。具体是不是对效率提升有很大的帮助,则依据页面的规模会有不同的效果,是否使用就要看函数设计人员的具体需求了,这里也就是提一种策略。

总的来说,通过尽可能少地执行javascript,将更多的样式化的任务交给CSS,则浏览器的渲染引擎来完成,又可以进一步地优化该函数,假设对addClass、css的调用需要100ms的话,此次优化直接消灭了原有120+726=846次的操作,节约了84600ms的时间(当然有夸张的成分,但是对整个函数的消耗来说,这个确实是很大的一块)。

这篇文章,仅仅是想在jQuery的各个实现的层面上来进行优化,只涉及到了对jQuery整个运行过程的分析、细节介绍和优化方向,并没有提到一些基本之基本的优化方法,比如:先将整个table从DOM树中移除,完成所有的操作之后再放回DOM,减少repaint。将mouseover和mouseout改为mouseenter和mouseleave,减少因为下正确的事件冒泡模型导致的重复的事件函数的执行。对于th、td之类单纯元素的选择,优先考虑使用原生的getElementsByTagName,消灭sizzle分析选择器的时间。

最后,这篇文章只是想说明,对于前端开发人员,虽然浏览器可能是个黑盒,但是很多框架、工具、库都是开放的,在使用之前如果可以进行一定程度的了解,必然有助于个人的技术提升和最终产品的质量优化,“知其然而不知其所以然”是非常忌讳的情况。

Javascript 相关文章推荐
javaScript 数值型和字符串型之间的转换
Jul 25 Javascript
不同浏览器对回车提交表单的处理办法
Feb 13 Javascript
jQuery EasyUI 开源插件套装 完全替代ExtJS
Mar 24 Javascript
js输出阴历、阳历、年份、月份、周示例代码
Jan 29 Javascript
js下拉选择框与输入框联动实现添加选中值到输入框的方法
Aug 17 Javascript
JS中的作用域链
Mar 01 Javascript
JS实现两周内自动登录功能
Mar 23 Javascript
jQuery实现打开网页自动弹出遮罩层或点击弹出遮罩层功能示例
Oct 19 jQuery
Angular2整合其他插件的方法
Jan 20 Javascript
vue内置指令详解
Apr 03 Javascript
vue主动刷新页面及列表数据删除后的刷新实例
Sep 16 Javascript
javascript浅层克隆、深度克隆对比及实例解析
Feb 09 Javascript
jquery的$getjson调用并获取远程的JSON字符串问题
Dec 10 #Javascript
如何用ajax来创建一个XMLHttpRequest对象
Dec 10 #Javascript
iframe 上下滚动条如何默认在下方实现原理
Dec 10 #Javascript
Ajax执行顺序流程及回调问题分析
Dec 10 #Javascript
js切换div css注意的细节
Dec 10 #Javascript
不同的jQuery API来处理不同的浏览器事件
Dec 09 #Javascript
addEventListener和attachEvent二者绑定的执行函数中的this不相同
Dec 09 #Javascript
You might like
PHP面向对象分析设计的经验原则
2008/09/20 PHP
FastCGI 进程意外退出造成500错误
2015/07/26 PHP
简单实现php上传文件功能
2017/09/21 PHP
Ajax中的JSON格式与php传输过程全面解析
2017/11/14 PHP
jQuery开发者都需要知道的5个小技巧
2010/01/08 Javascript
JS 对象介绍
2010/01/20 Javascript
JQuery中$.ajax()方法参数详解及应用
2013/12/12 Javascript
jquery简单实现图片切换效果的方法
2015/05/12 Javascript
JavaScript获取function所有参数名的方法
2015/10/30 Javascript
jQuery实现选中弹出窗口选择框内容后赋值给文本框的方法
2015/11/23 Javascript
Vue-Router模式和钩子的用法
2018/02/28 Javascript
vue实现axios图片上传功能
2019/08/20 Javascript
JS精确判断数据类型代码实例
2019/12/18 Javascript
javascript设计模式 ? 桥接模式原理与应用实例分析
2020/04/13 Javascript
vue项目中使用多选框的实例代码
2020/07/22 Javascript
使用vue编写h5公众号跳转小程序的实现代码
2020/11/27 Vue.js
怎样使用Python脚本日志功能
2016/08/14 Python
浅谈Pandas:Series和DataFrame间的算术元素
2018/12/22 Python
python 将大文件切分为多个小文件的实例
2019/01/14 Python
python分数表示方式和写法
2019/06/26 Python
Python 格式化输出_String Formatting_控制小数点位数的实例详解
2020/02/04 Python
Python闭包装饰器使用方法汇总
2020/06/29 Python
Python中的With语句的使用及原理
2020/07/29 Python
python实现数学模型(插值、拟合和微分方程)
2020/11/13 Python
python接口自动化框架实战
2020/12/23 Python
一款恶搞头像特效的制作过程 利用css3和jquery
2014/11/21 HTML / CSS
美国NBA官方商店:NBA Store
2019/04/12 全球购物
美国孩之宝玩具官网:Hasbro Pulse
2019/06/24 全球购物
数据库方面面试题
2012/04/22 面试题
女娲补天教学反思
2014/02/05 职场文书
投资合作意向书范本
2015/05/08 职场文书
2019暑假阅读倡议书
2019/06/24 职场文书
话题作文之自信作文
2019/11/15 职场文书
详解MySQL集群搭建
2021/05/26 MySQL
jQuery实现广告显示和隐藏动画
2021/07/04 jQuery
Python 如何利用ffmpeg 处理视频素材
2021/11/27 Python