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引用指针使用介绍
Nov 07 Javascript
jQuery中insertAfter()方法用法实例
Jan 08 Javascript
AngularJS学习笔记之TodoMVC的分析
Feb 22 Javascript
js获取浏览器高度 窗口高度 元素尺寸 偏移属性的方法
Nov 21 Javascript
javascript垃圾收集机制的原理分析
Dec 08 Javascript
codeMirror插件使用讲解
Jan 16 Javascript
xmlplus组件设计系列之列表(4)
Apr 26 Javascript
vue之数据交互实例代码
Jun 20 Javascript
vue车牌号校验和银行校验实战
Jan 23 Javascript
node中使用log4js4.x版本记录日志的方法
Aug 20 Javascript
TypeScript高级用法的知识点汇总
Dec 17 Javascript
原生js实现九宫格拖拽换位
Jan 26 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 fsockopen中多线程问题的解决办法[翻译]
2011/11/09 PHP
基于yaf框架和uploadify插件,做的一个导入excel文件,查看并保存数据的功能
2017/01/24 PHP
利用Homestead快速运行一个Laravel项目的方法详解
2017/11/14 PHP
jquery多选项卡效果实例代码(附效果图)
2013/03/23 Javascript
js判断FCKeditor内容是否为空的两种形式
2013/05/14 Javascript
解析jquery中的ajax缓存问题
2013/12/19 Javascript
用Jquery实现滚动新闻
2014/02/12 Javascript
textarea不能通过maxlength属性来限制字数的解决方法
2014/09/01 Javascript
JavaScript实现的SHA-1加密算法完整实例
2016/02/02 Javascript
Vue实现数字输入框中分割手机号码的示例
2017/10/10 Javascript
vue router-link传参以及参数的使用实例
2017/11/10 Javascript
react-router v4如何使用history控制路由跳转详解
2018/01/09 Javascript
微信小程序自定义组件封装及父子间组件传值的方法
2018/08/28 Javascript
记录vue项目中遇到的一点小问题
2019/05/14 Javascript
[42:06]2019国际邀请赛全明星赛 8.23
2019/09/05 DOTA
Python切片知识解析
2016/03/06 Python
如何在Django中设置定时任务的方法示例
2019/01/18 Python
Python文件路径名的操作方法
2019/10/30 Python
TensorFlow命名空间和TensorBoard图节点实例
2020/01/23 Python
Python发送手机动态验证码代码实例
2020/02/28 Python
Python内置方法和属性应用:反射和单例(推荐)
2020/06/19 Python
python requests库的使用
2021/01/06 Python
CSS3弹性盒模型开发笔记(三)
2016/04/26 HTML / CSS
html5本地存储之localstorage 、本地数据库、sessionStorage简单使用示例
2014/05/08 HTML / CSS
基于HTML5代码实现折叠菜单附源码下载
2015/11/27 HTML / CSS
日本最大的药妆连锁店:Matsukiyo松本清药妆店
2017/11/23 全球购物
优秀老师事迹材料
2014/02/05 职场文书
优秀应届毕业生推荐信
2014/02/18 职场文书
推荐信模板
2014/05/09 职场文书
宾馆前台接待岗位职责
2015/04/02 职场文书
怎样写家长意见
2015/06/04 职场文书
创业计划书之电动车企业
2019/10/11 职场文书
使用 JavaScript 制作页面效果
2021/04/21 Javascript
Oracle表空间与权限的深入讲解
2021/11/17 Oracle
mysql 联合索引生效的条件及索引失效的条件
2021/11/20 MySQL
python manim实现排序算法动画示例
2022/08/14 Python