在web應(yīng)用中区岗,DOM操作一直屬于是最常見的性能瓶頸厨钻,優(yōu)化DOM操作就可以大幅度提升應(yīng)用的速度,現(xiàn)今火熱的React中所使用的虛擬DOM這一賣點也是為了盡量減少DOM操作而存在的優(yōu)化方案,這一部分我們來具體說一說在DOM編程中的優(yōu)化方案。
DOM與JavaScript
通常瀏覽器會將DOM與JavaScript獨立實現(xiàn)崇呵,那么當我們訪問DOM元素的時候?qū)嶋H上是一個獨立的功能連接到另一個功能,而這個連接自然會產(chǎn)生性能消耗馅袁,每一次的訪問都會產(chǎn)生消耗的話域慷,盡可能的減少訪問次數(shù)則成為優(yōu)化的必然途徑,那么具體有哪些方法呢汗销。
減少DOM訪問次數(shù)犹褒,盡量使用JavaScript處理
舉一個簡單的例子:
<pre>
//我希望在頁面上輸出 1-100
錯誤示范:
for(var i = 1;i≤100;i++){
document.getElementById('p1').innerHTML += ' ' + i;
}
</pre>
上面這段代碼當然可以在頁面上輸出1-100的數(shù)字,但是注意弛针,這里每次循環(huán)都會調(diào)用document.getElementById來訪問DOM元素叠骑,那這里這個循環(huán)就訪問了100次DOM。
簡單修改一下:
<pre>
var strs = '';
for(var i = 1;i≤100;i++){
strs += ' ' + i;
}
document.getElementById('p1').innerHTML = strs;
</pre>
同樣的效果削茁,但是我們只訪問了一次DOM宙枷,字符串的累積操作我們完全在JavaScript中做掉房。
使用局部變量存儲DOM引用
上例子:
//我希望點擊一下按鈕數(shù)字+1
function bindActions(){
document.getElementById('button1').onclick=function(){
document.getElementById('p1').innerHTML = parseInt(document.getElementById('p1').innerHTML) + 1;
//jQuery 的$('#p1').html( parseInt($('#p1').html) + 1 ) 和上面是一樣的
}
}
bindActions();
這個例子中慰丛,每次點擊按鈕都會讓瀏覽器重新訪問ID為P1的元素并讓其原本數(shù)字+1卓囚,要多次訪問的DOM元素,我們應(yīng)該建立局部變量保存該元素的引用璧帝,以此減少DOM的訪問捍岳。
//修改后
//我希望點擊一下按鈕數(shù)字+1
function bindActions(){
//原生js
var elm_button = document.getElementById('button1'),elm_textbox = document.getElementById('p1');
elm_button .onclick=function(){
elm_textbox .innerHTML = parseInt(elm_textbox .innerHTML) + 1;
}
//jQuery
var $button = $('#button1'),$textbox = $('#p1');
$button.on('click',function(){
$textbox.html( parseInt( $textbox.html() ) + 1 );
})
}
//js與jq的這兩個命名方式是我的個人習慣
}
bindActions();
這個優(yōu)化很簡單實在,不過我覺得很多人還是嫌麻煩在用jquery的時候繼續(xù)直接 $(selector)睬隶。锣夹。。還是要養(yǎng)成建立多次引用的局部變量好八涨薄R肌!
為HTML集合做緩存
常見的HTML集合有
- document.getElementsByTagName
- document.getElementsByName
- document.getElementsByClassName
- document.links
- document.images
- document.forms
這些都是返回HTML集合的屬性恤左,HTML集合是一個類數(shù)組對象贴唇,擁有與數(shù)組類似的length屬性,也能使用數(shù)組下標來獲取元素飞袋。
使用HTML集合的時候戳气,請盡量將其緩存,使用循環(huán)語句的時候也要將length緩存( 訪問HTML集合的length屬性比訪問數(shù)組的length屬性要慢很多 )巧鸭,舉個例子:
//bad
var elms_div = document.getElementsByTagName('div');
for(var i=0;i<elms_div.length;i++){
elms_div[i].innerHTML= i ;
}
//good
var elms_div = document.getElementsByTagName('div');
for(var i=0,len=elms_div.length;i<len;i++){
elms_div[i].innerHTML= i ;
}
另外一點要注意瓶您,HTML是具有實時性的,它會與文檔一直保持著聯(lián)系纲仍,意思即是你每次使用集合的時候呀袱,集合的數(shù)據(jù)都是最新的,用一段代碼解釋:
var divs = document.getElementsByTagName('div');
console.log(divs.length); // 3
document.body.appendChild(document.createElement('div'));
console.log(divs.length); //4
每次訪問集合的時候他都會重新執(zhí)行查詢DOM的操作來返回最新的集合數(shù)據(jù)郑叠,這是需要注意的夜赵。
如果要使用的HTML集合的元素很多并且要頻繁操作,可以將集合內(nèi)的元素全部復(fù)制到數(shù)組中乡革,數(shù)組的速度要比HTML集合快得多( 因為集合與文檔時刻保持連接嘛 )
標準瀏覽器的原生DOM API
現(xiàn)代瀏覽器的原生JS中已經(jīng)提供了一些速度更快的原生DOM方法寇僧,如querySelectorAll();
這個方法用起來很爽署拟,類似使用css選擇器:
var elms = document.querySelectorAll('#d1 p');
不僅方便婉宰,而且這個方法返回的不是HTML集合,而是一個類數(shù)組的對象NodeList推穷,也正因為它不是HTMl集合心包,那么它自然就不會有集合的性能問題( 實時連接 )
重繪與重排
這一個點我覺得應(yīng)該是大部分人都不會關(guān)注的,不說關(guān)注馒铃,就連知道什么是重繪和重排的人都挺少的我估計= =蟹腾。
什么是重繪
要解釋這一點痕惋,我覺得我應(yīng)該先畫個圖
在DOM樹中每個需要被顯示的節(jié)點(display:none的元素不會在渲染樹中)在渲染樹中都會有對應(yīng)的節(jié)點,CSS模型定義中娃殖,渲染樹節(jié)點被稱為" frames " 或 "boxes" 值戳,上圖基本就是瀏覽器在獲取到資源后繪制頁面的過程,可能會有點不同不過基本流程都差不多炉爆。
當DOM樹與渲染樹構(gòu)建完成之后就瀏覽器就會繪制頁面堕虹。
假如DOM產(chǎn)生了幾何變化,那么與之對應(yīng)的渲染樹中的節(jié)點部分以及受到影響的部分都會失效芬首,然后進行重新構(gòu)建渲染樹赴捞,這個就是重排的過程
重繪很好理解,在重排之后瀏覽器重新繪制被影響至失效的部分郁稍,這個過程就是重繪
重排與重繪都會對程序UI產(chǎn)生影響赦政,盡量減少重排和重繪就是接下來我們要做的事。
什么時候會重排
布局與幾何屬性變動的時候瀏覽器就需要重排耀怜,對DOM元素的增加刪除恢着、改變位置、改變尺寸财破、改變內(nèi)容掰派、瀏覽器窗口尺寸修改都會產(chǎn)生重排,產(chǎn)生重排之后重繪是必然的左痢,但是反過來碗淌,重繪的發(fā)生并不一定是因為重排。
只要不對頁面布局以及幾何屬性修改就不需要重排抖锥,比如修改顏色只會發(fā)生重繪而并不需要重排(滾動條滾動會產(chǎn)生重排的哦)。
有興趣的同學可以去用下Google的SpeedTracer來觀測頁面渲染過程
上面說了那么多的重繪和重排碎罚,其實就是為了說明在使用JS的時候改變DOM會有什么影響磅废,如此看來,那么優(yōu)化方案的中心點其實與前面說的很類似荆烈,減少DOM操作是關(guān)鍵
合并對DOM元素的修改操作可以優(yōu)化拯勉,其次,減少重排還有下面幾種:
- 將元素先隱藏(display:none)憔购,更新完畢后再顯示出來宫峦,舉個例子:
常見的列表按條件排序、批量增加或刪除縱列玫鸟,先將其隱藏再排序結(jié)束后再放出导绷,會比直接操縱列表省去更多的重排
使用createDocumentFragment() 來更新節(jié)點。這個方法比較少人用屎飘,createDocumentFragment是一個document對象妥曲。它像是一個節(jié)點贾费,我們可以朝里面添加子節(jié)點,然后使用appendChild(fragment)將其加入目標上檐盟,被添加進去的會是fragment的子節(jié)點褂萧,并且添加到目標對象的過程中只會觸發(fā)一次重排且只訪問一次實時DOM。
為需要修改的節(jié)點做一個備份葵萎,然后操作副本导犹,操作結(jié)束后替換舊的即可。簡單概括就是先cloneNode羡忘,然后修改clone的谎痢,最后replaceChildj即可。
注意DOM動畫
利用元素制作一些動畫非常常見壳坪,這里要提到的是舶得,在做動畫的時候要使用absolute,脫離了文檔流之后就算元素改變也只是小范圍的重排重繪爽蝴,否則處于文檔流中變化的話沐批,會產(chǎn)生大面積的多次重排重繪動作。
事件委托
使用事件委托來減少監(jiān)聽處理器的數(shù)量是非常有必要的蝎亚,大量的事件綁定會讓瀏覽器花費大量的資源來跟蹤事件處理器九孩。
父元素可以通過冒泡接收到其下所有元素的事件消息,通過這個特性发框,我們可以將多數(shù)的事件處理器綁定在父元素上躺彬,通過篩選是否需要觸發(fā)的元素來觸發(fā)事件。
舉個例子:
//我想點擊頁面所有a標簽彈出hello
//這里直接用jQ演示梅惯。宪拥。
//bad
$('a').on('click',function(){
alert('hello')
});
//good
$(document.body).delegate('a','click',function(){
alert('hello')
})
上述代碼中兩者都實現(xiàn)了點擊a標簽彈出hello的功能,但是代碼二只監(jiān)聽了body就達到了這個效果铣减,而代碼一則給每個a標簽都綁定了事件監(jiān)聽器她君,孰優(yōu)孰劣不言而喻。