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 相关文章推荐
学习YUI.Ext 第二天
Mar 10 Javascript
JavaScript 继承的实现
Jul 09 Javascript
StringTemplate遇见jQuery冲突的解决方法
Sep 22 Javascript
javascript实现复选框选中属性
Mar 25 Javascript
深入理解JavaScript中的call、apply、bind方法的区别
May 30 Javascript
js判断文件格式及大小的简单实例(必看)
Oct 11 Javascript
JavaScript浏览器对象模型BOM(BrowserObjectModel)实例详解
Nov 29 Javascript
Vue2学习笔记之请求数据交互vue-resource
Feb 23 Javascript
JavaScript 中 apply 、call 的详解
Mar 21 Javascript
vue进行图片的预加载watch用法实例讲解
Feb 07 Javascript
Vue+Element实现动态生成新表单并添加验证功能
May 23 Javascript
vue element table中自定义一些input的验证操作
Jul 18 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
phpBB BBcode处理的漏洞
2006/10/09 PHP
PHP IN_ARRAY 函数使用注意事项
2010/07/24 PHP
PHP+SQL 注入攻击的技术实现以及预防办法
2010/12/29 PHP
win7计划任务定时执行PHP脚本设置图解
2014/05/09 PHP
PHP中的排序函数sort、asort、rsort、krsort、ksort区别分析
2014/08/18 PHP
PHP去除字符串最后一个字符的三种方法实例
2017/03/01 PHP
prototype 1.5相关知识及他人笔记
2006/12/16 Javascript
js刷新框架子页面的七种方法代码
2008/11/20 Javascript
javascript面向对象之Javascript 继承
2010/05/04 Javascript
js动态加载以及确定加载完成的代码
2011/07/31 Javascript
JQuery操作iframe父页面与子页面的元素与方法(实例讲解)
2013/11/20 Javascript
JavaScript的History API使搜索引擎抓取AJAX内容
2015/12/07 Javascript
JS选取DOM元素的简单方法
2016/07/08 Javascript
详解JavaScript树结构
2017/01/09 Javascript
javascript 面向对象function详解及实例代码
2017/02/28 Javascript
浅谈react-router HashRouter和BrowserRouter的使用
2017/12/29 Javascript
node.js +mongdb实现登录功能
2020/06/18 Javascript
[01:19:46]DOTA2-DPC中国联赛 正赛 SAG vs DLG BO3 第一场 2月28日
2021/03/11 DOTA
python标准日志模块logging的使用方法
2013/11/01 Python
Python简单连接MongoDB数据库的方法
2016/03/15 Python
python异常和文件处理机制详解
2016/07/19 Python
Python编写Windows Service服务程序
2018/01/04 Python
python实现Dijkstra算法的最短路径问题
2019/06/21 Python
python实现IOU计算案例
2020/04/12 Python
Python如何对齐字符串
2020/07/30 Python
用canvas做一个DVD待机动画的实现代码
2019/04/12 HTML / CSS
BONIA波尼亚新加坡官网:皮革手袋,鞋类和配件
2016/08/25 全球购物
Public Desire美国/加拿大:全球性的在线鞋类品牌
2018/12/17 全球购物
Java语言的优势
2015/01/10 面试题
机电一体化毕业生求职信
2013/11/02 职场文书
出纳员岗位职责
2014/03/13 职场文书
2014最新房贷收入证明范本
2014/09/12 职场文书
公司领导九九重阳节发言稿2014
2014/09/25 职场文书
小学远程教育工作总结
2015/08/13 职场文书
PostgreSQL并行计算算法及参数强制并行度设置方法
2022/04/06 PostgreSQL
蓝牙耳机怎么连接电脑win11? Win11蓝牙耳机连接电脑的技巧
2023/01/09 数码科技