JQuery each()函數如何優化循環DOM結構的性能

JQuery each()函數如何優化循環DOM結構的性能,each()方法能使DOM循環結構簡潔,不容易出錯。each()函數封裝了十分強大的遍歷功能,使用也很方便,它可以遍歷一維數組、多維數組、DOM, JSON 等等在javaScript開發過程中使用$each可以大大的減輕我們的工作量
關鍵字:JQuery、each()函數
如果對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分析選擇器的時間。

最后,這篇文章只是想說明,對于前端開發人員,雖然瀏覽器可能是個黑盒,但是很多框架、工具、庫都是開放的,在使用之前如果可以進行一定程度的了解,必然有助于個人的技術提升和最終產品的質量優化,“知其然而不知其所以然”是非常忌諱的情況。
众人帮太赚钱了