高性能JavaScript閱讀簡記(二)

3、DOM Scripting DOM編程

咱們都知道對DOM操做的代價昂貴,這每每成爲網頁應用中的性能瓶頸。在解決這個問題以前,咱們須要先知道什麼是DOM,爲何他會很慢。css

DOM in the Browser World 瀏覽器中的DOM

DOM是一個獨立於語言的,使用XMLHTML文檔操做的應用程序接口(API)。瀏覽器中多與HTML文檔打交道,DOM APIs也多用於訪問文檔中的數據。而在瀏覽器的實現中,每每要求DOM和JavaScript相互獨立。例如在IE中,JavaScript的實現位於庫文件jscript.dll中,而DOM的實現位於另外一個庫mshtml.dll中(內部代號Trident),這也是爲何IE內核是Trident,IE前綴爲-ms-,固然,咱們說的瀏覽器內核其實英文名叫 Rendering Engine/Layout Engine,準確翻譯應該是渲染引擎/排版引擎/模板引擎(實際上是一個東西);另一個就是JavaScript引擎,ie6-8採用的是JScript引擎,ie9採用的是Chakra。而這是相分離的;Chrome中Webkit的渲染引擎和V8的JavaScript引擎,Firefox中Spider-MonkeyJavaScript引擎和Gecko的渲染引擎,都是相互分離的。
Inherently Slow 天生就慢
由於上文所說的,瀏覽器渲染引擎JavaScript引擎是相互獨立的,那麼二者之間以功能接口相互鏈接就會帶來性能損耗。曾有人把DOMECMAScript(JavaScript)比喻成兩個島嶼,之間以一座收費橋鏈接,每次ECMAScript須要訪問DOM時,都須要過橋,交一次「過橋費」,操做DOM的次數越多,費用就越高,這裏的費用咱們能夠看做性能消耗。所以請盡力減小操做DOM的次數。
1. DOM Access and Modification DOM訪問和修改
訪問DOM的代價昂貴,修改DOM的代價可能更貴,由於修改會致使瀏覽器從新計算頁面的幾何變化,更更更貴的是採用循環訪問或者修改元素,特別是在HTML集合中進行循環。簡單舉例:html

function innerHTMLLoop(){
    for ( var count = 0; count < 100000; count++){
        document.getElementById("p").innerHTML += "-";
    }
}

這時候,每執行一次for循環,就對DOM進行了一次讀操做和寫操做(訪問和修改);此時咱們能夠採用另一種方式:html5

function innerHTMLLoop(){
    var content = "";
    for ( var count = 0; count < 100000; count++){
        content += "-";
    }
    document.getElementById("p").innerHTML += content;
}

咱們使用了一個局部變量存儲更新後的內容,在循環結束時一次性寫入,這時候只執行了一次讀操做和寫操做,性能提高顯著。所以,儘可能少的操做DOM,若是能夠在JavaScript範圍內完成的話。
2. innerHTML Versus DOM methods innerHTML與DOM方法
在老版本瀏覽器中,innerHTML更快但差異不大,更新的瀏覽器中,不相上下,最新的瀏覽器中,DOM方法更快,但依然差異不大。
3. Cloning Nodes 節點克隆
這樣的方法和DOM方法操做速度不相上下。node

HTML Collections HTML集合

HTMLCollection是用於存放DOM節點引用的類數組對象。獲得的方法有:document.getElementByName/document.getElementByClassName/document.getElementByTagName/document.querySelectAll/document.images/document.links/document.forms等;也有相似於document.forms[0].elements(頁面第一個表單的全部字段)等。
上面這些方法返回HTMLCollection對象,是一種相似數組的列表,沒有數組的方法,可是有lenth屬性。在DOM標準中定義爲:「虛擬存在,意味着當底層文檔更新時,他們將自動更新」。HTML集合實際上會去查詢文檔,更新信息時,每次都要重複執行這種查詢操做,這正是低效率的來源。web

  • Expensive collections 昂貴的集合
    先看個例子:編程

var oDiv = document.getElementByTagName("div");
for (var i = 0; i < oDiv.length; i++){
    document.body.appendChild(document.createElement("div"))
}

好吧,這是個死循環,永遠不會結束,可是這個過程當中,每訪問一次oDiv.length,就會從新計算一遍其長度,固然,前提是對全部的div從新進行一次遍歷,所以,在這裏,咱們最好使用一個變量暫存oDIv.lengthsegmentfault

var oDivLen = document.getElementByTagName("div").length;
for (var i = 0; i < oDivLen; i++){
    document.body.appendChild(document.createElement("div"))
}

從性能角度來說,這樣作會快不少。同時,由於對HTML集合的訪問比對數組訪問要更耗費性能,所以在某些不得很少次訪問HTML集合的狀況下,能夠先將集合存儲爲一個數組,而後對數組進行訪問:數組

function toArray(htmlList){
    for (var i = 0, htmlArray = [], len = htmlList.length; i < len; i++){
        htmlArray[i] = htmlList[i];
    }
    return htmlArray;
}

固然,這也須要額外的開銷,須要本身進行權衡是否有必要這樣作。瀏覽器

  • Local variables when accessing collection elements 訪問集合元素時使用局部變量
    對於任何類型的DOM訪問,若是對同一個DOM屬性或者方法訪問屢次,最好使用一個局部變量對此DOM成員進行緩存。特別是在HTML集合中訪問元素時,若是屢次對集合中的某一元素訪問,一樣須要將這個元素先進行緩存。緩存

Walking the DOM DOM漫談

DOM API提供了多種訪問文檔結構特定部分的方法,去選擇最有效的API。

  • Crawling the DOM 抓取DOM
    若是你能夠經過:document.getElementByID();得到某元素就不要去用document.getElementById().parentNode;這麼麻煩去獲取。若是你可已經過nextSibling去獲取一個元素就不要經過childNodes去獲取,由於後者是一個nodeList集合。

  • Element nodes 元素節點
    DOM包含三個節點(也能夠說是四個):元素節點、屬性節點、文本節點(以及註釋節點);一般狀況下,咱們獲取到和使用的是元素節點,可是咱們經過childNodes、firstChild、nextSibling等方法獲取到的是全部節點的屬性,js中有一些其餘的API能夠用來只返回元素節點或者元素節點的某些屬性,咱們能夠用這些API取代那些返回整個所有節點或者節點屬性的API,例如:

<s>childNodes</s>                        children
<s>childNodes.length</s>                childElementCount
<s>firstChild</s>                        firstElementChild
<s>lastChild</s>                        lastElementChild
<s>nextSibling</s>                        nextElementSibling
<s>previousSibling</s>                    previousElementSibling

在全部的瀏覽器中,後者比前者要快,只不過IE中後面部分方法並不支持,好比IE6/7/8,只支持children方法。

  • The Selectors API 選擇器API
    傳統的選擇器在性能方面問題不大,只不過應用場景相對單一,當咱們用習慣了CSS選擇器以後,咱們會以爲DOM給咱們提供的選擇器讓咱們抓狂。在querySelector/querySelectorAll以前,若是咱們想要查找到元素下符合條件的另外一元素時,不得不使用相似下面的方法:document.getElementById("id1").getElementById("id2");,但若是你想獲取一個類名爲class1或類名爲class2的div的時候,不得不這麼處理:

function getDivClass1(className1,className2){
    var results = [];
    divs = document.getElementByTagName("div");
    for (var i = 0,len = divs.length; i < len; i++){
        _className = divs[i].className;
        if(_className === className1 || _className === className2){
            results.push(divs[i]);
        }
    }
    return results;
}

不只僅是由於冗長的代碼,屢次對DOM進行遍歷帶來的性能問題也不可小窺;不過在有了querySelector/querySelectorAll以後,這一切變得簡單,減小對DOM的遍歷也帶來了性能的提高。上面兩個例子能夠重寫以下:

document.querySelector("#id1 #id2");
document.querySelectorAll("div.className1,div.className2");

所以,若是能夠,儘可能使用querySelector/querySelectorAll吧。

Repaints and Reflows 重繪和重排(也稱迴流)

這涉及到一個比較古老的議題,瀏覽器在拿到服務器響應時都幹了什麼。我查閱了至關一部分資料(網上不少地方說法是不許確的,包括一些問答、博客),去了解整個流程,這裏簡單的描述一下過程。更多細節可參考《瀏覽器的工做原理:新式網絡瀏覽器幕後揭祕》,原版地址(http://taligarsiel.com/Projec...)。
上文提到過,瀏覽器的實現通常包括渲染引擎JavaScript引擎;兩者是相互獨立的。
咱們先從渲染引擎的角度來看一下在拿到服務器的文檔後的處理流程:

  • Parsing HTML to construct the DOM tree 解析HTML以構建DOM tree
    解析HTML文檔,將各個標記逐個轉化爲DOM tree 上的DOM節點;固然並非一一對應;相似於head這樣的標記是在DOM tree上是沒有對應的節點的。在這個過程當中,同時被解析的還包括外部CSS文件以及樣式元素中的樣式數據,這些數據信息被準備好進行下一步工做。

  • Render tree construction 構建render tree
    DOM tree構建過程當中,CSS文件同時被解析,DOM tree上每個節點的對應的顏色尺寸等信息被保存在另外一個被稱做rules tree的對象中(具體實現方式webkitgecho是不同的,可參考上文提到過的《瀏覽器的工做原理》)。DOM treerules tree二者一一對應,均構建完成以後,render tree也就構建完成了。

  • Layout of the render tree 佈局render tree
    依據render tree中的節點信息和對應的rules中的尺寸信息(包括display屬性等),爲每個節點分配一個應該出如今屏幕上的確切座標。

  • Painting the render tree 繪製render tree
    就是將已佈局好的節點,加上對應的顏色等信息,繪製在頁面上。

固然,瀏覽器並不會等到所有的HTML文檔信息都拿到以後才進行解析,也不會等到所有解析完畢以後纔會進行構建render tree設置佈局。渲染引擎可能在接收到一部分文檔後就開始解析,在解析了一部分文檔後就開始進行構建render treelayout render tree
JavaScript引擎的工做:
正常的流程中,渲染引擎在遇到<script>標記時,會中止解析,由JavaScript引擎當即執行腳本,若是是外部則當即進行下載並執行,這期間渲染引擎是不工做的。若是腳本被標註爲defer時,腳本當即進行下載,但暫不執行,並且這期間也不會阻塞渲染引擎的渲染,腳本會在文檔解析完畢以後執行;若是腳本被標註爲async時,腳本當即進行下載,下載完成後會當即執行,只不過這個過程和渲染時兩個線程進行的,兩者是異步執行,一樣不會影響頁面渲染。deferasync兩者的區別是:雖然都是當即下載(這兩個都只做用於外部腳本),可是前者是在文檔解析完畢後執行,後者是下載完成後當即執行,所以前者能夠保證腳本按照順序執行,然後者誰先下載完誰先執行會致使依賴關係混亂。
注:關於asyncdefer,在查閱資料後的說法見上文。但在我本身編寫DEMO測試的過程當中,發如今Chrome中偶爾會所有阻塞,偶爾都不阻塞,最多的是腳本以上的文檔不阻塞,如下的文檔被阻塞;IE/FireFox中徹底不鳥這倆屬性,直接等到腳本執行完畢纔出現頁面。
說回重繪和重排,
先說重排,當render tree中的一部分由於元素的尺寸、佈局、顯示隱藏等改變而須要從新構建render tree時,此時就叫重排,也就是從新構建render tree;會引發重排的場景包括:添加或者刪除可見的DOM元素、元素位置改變、元素尺寸改變(邊距、填充、邊框、寬高等)、內容改變(所以引發尺寸改變)、頁面渲染初始化(首次加載頁面)、瀏覽器窗口尺寸改變;還有很重要的一點就是在嘗試獲取頁面元素的尺寸時也會強制引起重排(由於重排的過程會從新計算元素的尺寸,因此爲保證得到最新的尺寸信息,會先強制進行重排),例如:offsetTop/offsetLeft/offsetWidth/offsetHeight/scrollTop/scrollLeft/scrollWidth/scrollHeight/clientTop/clientLeft/clientWidth/clientHeight/width/height/getComputedStyle()/currentStyle()
重繪通常發生在元素的外觀變化時,首先重排必定會引發重繪,當元素的顏色/背景色/透明度/visibility屬性等發生變化也會引發重繪。

  • Queuing and Flushing Render Tree Changes 查詢並刷新render tree改變
    前文咱們知道了構建render tree過程當中,會先對CSS以及樣式元素中的樣式數據進行解析計算,在引起重排時,會對樣式數據從新計算,性能問題就出如今大量計算的過程當中,在大多數的瀏覽器中,會經過隊列化修改和批量顯示優化重排的過程,可是咱們剛所提到的嘗試獲取頁面尺寸信息會強制引起重排,相似下面代碼:

var computedValue,
    tmp = '',
    bodystyle = document.body.style; 
    if (document.body.currentStyle) { 
        computedValue = document.body.currentStyle; 
    }else{  
        computedValue = document.defaultView.getComputedStyle(document.body, '');
    } 
    bodystyle.color = 'red'; 
    tmp = computedValue.backgroundColor; 
    bodystyle.color = 'white'; 
    tmp = computedValue.backgroundImage; 
    bodystyle.color = 'green'; 
    tmp = computedValue.backgroundAttachment;

上面示例中,body的字體顏色被改變了三次,每次改變後都對body的樣式信息進行了查詢,雖然查詢的信息和字體顏色無關,可是瀏覽器會所以刷新渲染隊列並進行重排,因此共進行了三次重排,也理所固然的進行了三次重繪,這裏能夠改進一下:

var computedValue,
    tmp = '',
    bodystyle = document.body.style; 
    if (document.body.currentStyle) { 
        computedValue = document.body.currentStyle; 
    }else{  
        computedValue = document.defaultView.getComputedStyle(document.body, '');
    } 
    bodystyle.color = 'red'; 
    bodystyle.color = 'white'; 
    bodystyle.color = 'green';
    tmp = computedValue.backgroundColor; 
    tmp = computedValue.backgroundImage; 
    tmp = computedValue.backgroundAttachment;

在下面的例子中,實際上引發了一次重排和兩次重繪,首先bodystyle.color的三次變化被批量化一次處理,只進行了一次重繪,接着對computedValue的訪問批量處理,進行了一次重排,接着這次重排又引發一次重繪。速度要比優化以前的更快。所以,儘可能不要在佈局信息發生變化時對元素尺寸進行查詢。

  • Minimizing Repaints and Reflows 最小化重繪和重排
    上面的例子其實就是減少重繪重排的一種方法,儘可能將對DOM和風格改變的操做放在一塊兒,在一次批量修改中完成。

① style changes 改變風格
先舉例:

var oDiv =  document.getElementById('div');
oDib.style.borderLeft = "1px";
oDib.style.padding = "10px";
oDib.style.width = "100px";

這裏對oDiv進行了三次改變,每次改變都涉及到元素的幾何屬性,雖然大多數瀏覽器進行了優化,只進行一次重排,但部分老式瀏覽器中,效率很低,並且若是在此時進行了佈局信息查詢,會致使三次重排的進行,咱們能夠換一種風格實現:

var oDiv =  document.getElementById('div');
oDiv.style.cssText = "border-left: 1px;padding: 10px;width: 100px;";

優化後的代碼,只進行一次重排,可是會覆蓋原有的樣式信息(這裏的覆蓋會清空原來全部的行內樣式信息),所以也能夠這麼寫:

oDiv.style.cssText += ";border-left: 1px;padding: 10px;width: 100px;";

固然,咱們也能夠經過對類名的修改,類名事先在CSS中定義了對應的樣式信息,來達到修改樣式的需求,好比:

var oDiv =  document.getElementById('div');
    oDiv.className += "current";

② Batching DOM changes 批量修改DOM
原文翻譯是批量修改DOM,個人理解是批量DOM修改,這種方法是將會被屢次修改的DOM元素,先從文檔流摘除,而後批量修改,而後帶回文檔,這樣僅僅在摘除和帶回時發生兩次重排,中間的屢次修改,並不會帶來重排。將元素從文檔摘除有不少方法,好比將元素隱藏(dispaly:none;)、DOM以外新建一個文檔片斷修改後添加到原節點位置。
③ Caching Layout Information 緩衝佈局信息
好比咱們經過setTimeout實現一個動畫:元素每10毫秒向右下方移動1px;從100X100移動到500X500

myElement.style.left = 1 + myElement.offsetLeft + 'px'; 
myElement.style.top = 1 + myElement.offsetTop + 'px'; 
if (myElement.offsetLeft >= 500){
    stopAnimation();
}

代碼中,咱們每次查詢myElement.offsetLeft/myElement.offsetTop值的時候,都引發了一次頁面重排,一次循環中,至少進行了三次重排,性能糟糕的不要不要的,咱們能夠經過優化,將myElement.offsetLeft/myElement.offsetTop的值緩存起來:

var current = myElement.offsetLeft;

循環內:

current++;
myElement.style.left = current + 'px';
myElement.style.top = current + 'px';
if (current >= 500) { 
        stopAnimation();
    }

myElement.offsetLeft緩存起來,初始查詢一次,以後再也不進行查詢,只引用變量current;並且在瀏覽器的優化後,每次循環只進行了一次重排,性能提高不是一點。
④ Take Elements Out of the Flow for Animations 將元素提出動畫流
顯示/隱藏部分頁面、摺疊/展開動畫是很常見的動畫交互模式。不少時候展開/摺疊動畫,會將頁面一部分擴大,將下面的部分向下推開,這樣,會帶來不只僅是展開部分的重排,包括下面的部分所有重排,而重排的性能消耗,和影響的渲染樹程度有關,所以咱們能夠減小對頁面影響部分來實現減少重排帶來的性能消耗。
(1) 使用絕對座標定位頁面動畫部分,使其脫離於頁面文檔佈局流以外,這樣它的改變對其餘文檔的位置尺寸等信息無影響,不會引發其餘部分重排
(2) 展開動做只在動畫元素上進行,其下面的元素不隨着動畫元素展開推移,只是被遮蓋,這樣也不會引發大範圍的渲染樹從新計算
(3) 在動畫結束後,再將下面的元素一次性移動,而不是動畫過程當中慢慢移動。
⑤ IE and :hover IE和:hover
IE7+全面支持:hover,可是大量元素使用:hover會帶來嚴重的性能問題。好比一個大型的表格,使用tr:hover來使鼠標光標所在行高亮時,會使cpu的使用率提高80%-90%。所以應當儘可能避免大量元素使用:hover屬性。

Event Delegaton 事件託管

當頁面上存在大量元素,並且每一個元素都有一個或者多個事件句柄與之綁定的時候,可能會影響性能,由於掛接每一個句柄都是有代價的,更多的頁面標記和JavaScript代碼,運行期須要訪問和修改更多的DOM節點。更重要的是事件掛載發生在onload事件中,而這個時間段是有不少事要處理的,無形中影響到其餘事件的處理。可是你給頁面上一百個元素每人綁定了一個點擊事件,可是可能只有十個可能會被真正點擊調用,作了90%的無用功。
咱們能夠經過事件託管來處理這類需求,原理是事件冒泡,也就是在包裹元素上掛接一個句柄,用於處理其子元素髮生的全部事件。好比在點擊時,判斷當前標籤的類名,不一樣類名執行對應的操做,這樣既不用給每個元素綁定事件句柄,也實現了每一個元素的點擊事件處理。Jquery中的on能夠給動態添加的元素綁定事件也是利用了事件託管的辦法。

高性能JavaScript閱讀簡記(一)
高性能JavaScript閱讀簡記(二)
高性能JavaScript閱讀簡記(三)

相關文章
相關標籤/搜索