精益求精 jQuery代碼的分析與優(yōu)化
今天剛回家,QQ群里就看到有人求助優(yōu)化一段jQuery代碼,簡(jiǎn)單看了一下,發(fā)現(xiàn)如果對(duì)jQuery這東西只停留在用的層面,而不知其具體實(shí)現(xiàn)的話,真的很容易用出問(wèn)題來(lái)。這也是為什么近期我一直不怎么推崇用jQuery,這框架的API設(shè)定就有誤導(dǎo)人們走上歧途之嫌。
需要優(yōu)化的代碼大致是這樣的,也不方便直接把人家的代碼復(fù)制過(guò)來(lái),就大概地表達(dá)下意思:
- $.fn.beautifyTable = function(options) {
- //定義默認(rèn)配置項(xiàng),再用options覆蓋
- return this.each(function() {
- var table = $(this),
- tbody = table.children('tbody'),
- tr = tbody.children('tr'),
- th = tbody.children('th'),
- td = tbody.children('td');
- //單獨(dú)內(nèi)容的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
- //對(duì)齊方式
- tr.children('th,td').css('text-align', options.align); //5
- //添加鼠標(biāo)懸浮
- tr.bind('mouseover', addActiveClass); //6
- tr.bind('mouseout', removeActiveClass); //7
- //點(diǎn)擊變色
- tr.bind('click', toggleClickClass); //8
- });
- };
總的來(lái)說(shuō),這段代碼不錯(cuò),思路清晰,邏輯明確,想要做什么也通過(guò)注釋說(shuō)得很明白了。但是按作者的說(shuō)法,當(dāng)表格中有120行時(shí),IE已經(jīng)反映腳本運(yùn)行時(shí)間過(guò)長(zhǎng)了。顯然從表現(xiàn)來(lái)看,這個(gè)函數(shù)的效率不高,甚至說(shuō)極其低下。
尋找原因
于是,開(kāi)始從代碼層面進(jìn)行分析,這是一個(gè)標(biāo)準(zhǔn)的jQuery插件式的函數(shù),有個(gè)典型的return this.each(function() { ... };);形式的代碼,如果作者寫下這段代碼的時(shí)候,不是照本宣科不經(jīng)思考的話,就應(yīng)該意識(shí)到j(luò)Query的一個(gè)函數(shù)干了什么事。
簡(jiǎn)單來(lái)說(shuō),jQuery.fn下的函數(shù),絕大部分是一個(gè)each的調(diào)用,所謂each,自然是對(duì)選擇出來(lái)的元素進(jìn)行了遍歷,并對(duì)某個(gè)元素進(jìn)行了指定的操作。那么看看上面一段代碼,進(jìn)行了多少的遍歷,在此就假設(shè)只選擇了120行,每一行有6列,另加上1行的表頭吧:
- · 遍歷th,添加headerClass,元素?cái)?shù)為6。
- · 遍歷td,添加cellClass,元素?cái)?shù)為6*120=720。
- · 從所有tr中找出奇數(shù)的,需要對(duì)所有tr進(jìn)行一次遍歷,元素?cái)?shù)為120。
- · 遍歷奇數(shù)的tr,添加evenRowClass,元素?cái)?shù)為120/2=60。
- · 從所有tr中找出偶數(shù)的,需要對(duì)所有tr進(jìn)行一次遍歷,元素?cái)?shù)為120。
- · 遍歷偶數(shù)的tr,添加oddRowClass,元素?cái)?shù)為120/2=60。
- · 遍歷所有th和td,添加text-align,元素?cái)?shù)為120*6+6=726。
- · 遍歷所有tr,添加mouseover事件,元素?cái)?shù)為120。
- · 遍歷所有tr,添加mouseout事件,元素?cái)?shù)為120。
- · 遍歷所有tr,添加click事件,元素?cái)?shù)為120。
為了方便,我們簡(jiǎn)單地假設(shè),在遍歷中訪問(wèn)一個(gè)元素耗時(shí)為10ms,那么這個(gè)函數(shù)一共用了多少時(shí)間呢?這個(gè)函數(shù)共遇上了2172個(gè)元素,耗時(shí)21720ms,即21秒,顯然IE確實(shí)應(yīng)該報(bào)腳本執(zhí)行過(guò)久了。
基本優(yōu)化
知道了效率低下的原因,要從根本上進(jìn)行解決,自然要想方設(shè)法來(lái)合并循環(huán),初略一看,按照上邊代碼中注釋里的數(shù)字,至少以下幾點(diǎn)是可以合并的:
- · 3和4可以合并為一次循環(huán),從120+60+120+60變?yōu)?20,減少了240。
- · 1、2和5可以合并為一次循環(huán),從6+720+726變?yōu)?26,減少了726。
- · 6、7、8可以合并為一次循環(huán),從120+120+120變?yōu)?20,減少了240。
- · 進(jìn)一步的,3、4和6、7、8一樣可以合并為一次循環(huán),繼續(xù)減少了120。
累加一下,我們一共減少了240+726+240+120=1326次元素操作,總計(jì)13260ms。在優(yōu)化之后,我們的函數(shù)耗時(shí)變?yōu)?1720-13260=8460ms,即8s。
注意選擇器
到這里可能會(huì)有一個(gè)疑問(wèn),從表格的結(jié)構(gòu)上來(lái)說(shuō),所有的th和td元素肯定都在tr之內(nèi),那么為什么不將1、2、5這三步的循環(huán)同樣放到對(duì)tr的循環(huán)中,形成一個(gè)嵌套的循環(huán),這樣不是更加快速嗎?
這里之所以沒(méi)有這么做,主要有2個(gè)原因:
其一,無(wú)論將1、2、5這三者放在哪里,都不會(huì)減少對(duì)所有th和td元素的一次訪問(wèn)。
另一方面,$('th,td')這個(gè)選擇器,在sizzle中會(huì)被翻譯成2次getElementsByTagName函數(shù)的調(diào)用,***次獲取所有th,第二次獲取所有td,然后進(jìn)行集合的歸并。由于getElementsByTagName是內(nèi)置函數(shù),在此可以認(rèn)為該函數(shù)是不帶循環(huán)的,即復(fù)雜度為O(1),同樣集合的歸并使用Array的相關(guān)函數(shù),是對(duì)內(nèi)存的操作,復(fù)雜度同樣為O(1)。
反之,如果在對(duì)tr元素的循環(huán)中再采用$('th,'td)這個(gè)選擇器,則是在tr元素上調(diào)用2次getElementsByTagName,由于無(wú)論在哪個(gè)元素上調(diào)用該函數(shù),函數(shù)執(zhí)行的時(shí)間是相同的,因此在循環(huán)tr時(shí)使用,反而多出了119*2次的函數(shù)調(diào)用,效率不升反降。
可見(jiàn),對(duì)sizzle選擇器的基本知識(shí),也是幫助優(yōu)化jQuery代碼的很重要的一方面。
不要啥都讓JavaScript來(lái)做
根據(jù)前面的基本的優(yōu)化,已經(jīng)將時(shí)間從21秒降到了8秒,但是8秒這個(gè)數(shù)字顯然是無(wú)法接受的。
再進(jìn)一步分析我們的代碼,事實(shí)上,循環(huán)遍歷是語(yǔ)言層面上的內(nèi)容,其速度應(yīng)該是相當(dāng)快的。而針對(duì)每個(gè)元素所做的操作,是jQuery提供的函數(shù),相比遍歷來(lái)說(shuō),才是占去大部分資源的主子。如果說(shuō)遍歷中訪問(wèn)元素用時(shí)是10ms的話,不客氣地說(shuō)執(zhí)行一個(gè)addClass至少是100ms級(jí)別的消耗。
因此,為了進(jìn)一步地優(yōu)化效率,就不得不從減少對(duì)元素的操作入手。再仔細(xì)地回審代碼,發(fā)現(xiàn)這個(gè)函數(shù)有著非常多的對(duì)樣式的修改,其中至少包括了:
- · 給所有th加上class。
- · 給所有td加上class。
- · 給tr分奇偶行加上class。
- · 給所有th和td加上一個(gè)text-align樣式。
而事實(shí)上我們知道,CSS本身就擁有子代選擇器,而瀏覽器原生對(duì)CSS的解析,效率遠(yuǎn)遠(yuǎn)高于讓javascript去給元素一一加上class。
所以,如果對(duì)CSS是可控的,那么這個(gè)函數(shù)就不應(yīng)該擁有headerClass、cellClass這兩個(gè)配置項(xiàng),而是盡可能地在CSS中進(jìn)行配置:
- .beautiful-table th { /* headerClass的內(nèi)容 */ }
- .beautiful-table td { /* cellClass的內(nèi)容 */ }
再者,對(duì)于tr的奇偶行樣式,在部分瀏覽器下可以使用:nth-child偽類來(lái)實(shí)現(xiàn),這方面可以利用特性探測(cè),僅在不支持該偽類的瀏覽器中使用addClass添加樣式。當(dāng)然如果你僅僅想對(duì)IE系列進(jìn)行優(yōu)化的話,這一條可以忽略了。
對(duì)于:nth-child偽類的探測(cè),可以用以下的思路來(lái)進(jìn)行:
- · 創(chuàng)建一個(gè)stylesheet,再創(chuàng)建一條規(guī)則,如#test span:nth-child(odd) { display: block; }。
- · 創(chuàng)建相應(yīng)的HTML結(jié)構(gòu),一個(gè)id為test的div,內(nèi)部放置3個(gè)span。
- · 將stylesheet和div一同加入的DOM樹(shù)中。
- · 查看第1和第3個(gè)span的運(yùn)行期display樣式,如果是block,則表明支持該偽類。
- · 刪除創(chuàng)建的stylesheet和div,別忘了緩存探測(cè)的結(jié)果。
***,對(duì)于給所有th和td元素添加text-align樣式,也是可以通過(guò)css進(jìn)行優(yōu)化的。既然不知道添加的是哪個(gè)align,那么就多寫幾個(gè)樣式:
- /* CSS樣式 */
- .beautiful-table-center th,.beautiful-table-center td { text-align: center !important; }
- .beautiful-table-right th,.beautiful-table-right td { text-align: right !important; }
- .beautiful-table-left th,.beautiful-table-left td { text-align: left !important; }
- /* javascript */
- table.addClass('beautiful-table-' + options.align);
當(dāng)然,上面所說(shuō)的優(yōu)化,是建立在對(duì)CSS有控制權(quán)的情況下的,如果本身無(wú)法接觸到CSS樣式,比如這是一個(gè)通用的插件函數(shù),會(huì)被完全無(wú)法控制的第三方使用,那么怎么辦呢?也不是完全沒(méi)有辦法:
- · 去找頁(yè)面里的所有CSS規(guī)則,比如document.styleSheets。
- · 遍歷所有規(guī)則,把配置項(xiàng)中的headerClass、cellClass等拿出來(lái)。
- · 提取需要的幾個(gè)class中的所有樣式,再自己組裝成新的選擇器,如beautiful-table th。
- · 使用創(chuàng)建出來(lái)的選擇器,生成新的stylesheet,加入到DOM樹(shù)中。
- · 那么只給table加上beautiful-table這個(gè)class就搞定了。
當(dāng)然上面的做法其實(shí)也蠻消耗時(shí)間的,畢竟又要遍歷stylesheet,又要?jiǎng)?chuàng)建stylesheet。具體是不是對(duì)效率提升有很大的幫助,則依據(jù)頁(yè)面的規(guī)模會(huì)有不同的效果,是否使用就要看函數(shù)設(shè)計(jì)人員的具體需求了,這里也就是提一種策略。
總的來(lái)說(shuō),通過(guò)盡可能少地執(zhí)行javascript,將更多的樣式化的任務(wù)交給CSS,則瀏覽器的渲染引擎來(lái)完成,又可以進(jìn)一步地優(yōu)化該函數(shù),假設(shè)對(duì)addClass、css的調(diào)用需要100ms的話,此次優(yōu)化直接消滅了原有120+726=846次的操作,節(jié)約了84600ms的時(shí)間(當(dāng)然有夸張的成分,但是對(duì)整個(gè)函數(shù)的消耗來(lái)說(shuō),這個(gè)確實(shí)是很大的一塊)。
***的補(bǔ)充
這篇文章,僅僅是想在jQuery的各個(gè)實(shí)現(xiàn)的層面上來(lái)進(jìn)行優(yōu)化,只涉及到了對(duì)jQuery整個(gè)運(yùn)行過(guò)程的分析、細(xì)節(jié)介紹和優(yōu)化方向,并沒(méi)有提到一些基本之基本的優(yōu)化方法,比如:
先將整個(gè)table從DOM樹(shù)中移除,完成所有的操作之后再放回DOM,減少repaint。
將mouseover和mouseout改為mouseenter和mouseleave,減少因?yàn)橄抡_的事件冒泡模型導(dǎo)致的重復(fù)的事件函數(shù)的執(zhí)行。
對(duì)于th、td之類單純?cè)氐倪x擇,優(yōu)先考慮使用原生的getElementsByTagName,消滅sizzle分析選擇器的時(shí)間。
***,這篇文章只是想說(shuō)明,對(duì)于前端開(kāi)發(fā)人員,雖然瀏覽器可能是個(gè)黑盒,但是很多框架、工具、庫(kù)都是開(kāi)放的,在使用之前如果可以進(jìn)行一定程度的了解,必然有助于個(gè)人的技術(shù)提升和最終產(chǎn)品的質(zhì)量?jī)?yōu)化,“知其然而不知其所以然”是非常忌諱的情況。
原文地址:http://www.otakustay.com/jquery-code-optimization-1/
【編輯推薦】