高性能JS-DOM

用腳本進行DOM操做的代價是很昂貴的,它是富web應用中最多見的性能瓶頸。主要有如下三種問題:css

  1. 訪問和修改DOM元素html

  2. 修改DOM元素的樣式致使repaint和reflow前端

  3. 經過DOM事件處理與用戶進行交互node

瀏覽器中的DOM

DOM是(Document Object Model)一個與語言無關的、用來操做XML和HTML文檔的應用程序接口(Application Program Interface)。 儘管DOM與語言無關,可是在瀏覽器中的接口倒是用JavaScript來實現的。web

一個前端小知識

瀏覽器一般會把js和DOM分開來分別獨立實現。
舉個栗子冷知識,在IE中,js的實現名爲JScript,位於jscript.dll文件中;DOM的實現則存在另外一個庫中,名爲mshtml.dll(Trident)。
Chrome中的DOM實現爲webkit中的webCore,但js引擎是Google本身研發的V8。
Firefox中的js引擎是SpiderMonkey,渲染引擎(DOM)則是Gecko。面試

DOM,天生就慢

前面的小知識中說過,瀏覽器把實現頁面渲染的部分和解析js的部分分開來實現,既然是分開的,一旦二者須要產生鏈接,就要付出代價。
兩個例子:數組

  1. 小明和小紅是兩個不一樣學校的學生,兩我的家裏經濟條件都不太好,買不起手機(好尷尬的設定Orz...),因此只能經過寫信來互相交流,這樣的過程確定比他倆面對面交談時所須要花費的代價大(額外的事件、寫信的成本等)。瀏覽器

  2. 官方例子:把DOM和js(ECMAScript)各自想象爲一座島嶼,它們之間用收費橋進行鏈接。ECMAScript每次訪問DOM,都要途徑這座橋,並交納「過橋費」。訪問DOM的次數越多,費用也就越高。緩存

所以,推薦的作法是:儘量的減小過橋的次數,努力待在ECMAScript島上。數據結構

DOM的訪問與修改

前面說到訪問DOM須要交納「過橋費」,而修改DOM元素則代價更爲昂貴,由於它會致使瀏覽器從新計算頁面的幾何變化。 來看一段代碼:

function innerHTMLLoop(){      for (var count = 0; count < 15000; count++){          document.getElementById('text').innerHTML += 'dom';      }   }

這段代碼,每次循環會訪問兩次特定的元素:第一次讀取這個元素的innerHTML屬性,第二次重寫它。
看清楚了這一點,不可貴到一個效率更高的版本:

function innerHTMLLoop2(){      var content = '';      for (var count = 0; count < 15000; count++){          content += 'dom';      }      document.getElementById('text').innerHTML += content;   }

用一個局部變量包層每次更新後的內容,等待循環結束後,一次性的寫入頁面(儘量的把更多的工做交給js的部分來作)。
根據統計,在全部的瀏覽器中,修改後的版本都運行的更快(優化幅度最明顯的是IE8,使用後者比使用前者快273倍)。

HTML元素集合

HTML元素集合是包含了DOM節點引用的類數組對象。
能夠用如下方法或屬性獲得一個HTML元素集合:

  • document.getElementsByName()

  • document.getElementsByTagName()

  • document.getElementsByClassName()

  • document.images 頁面中全部img元素

  • document.links 頁面中全部a元素

  • document.forms 頁面中全部表單元素

  • document.forms[0].elements 頁面中第一個表單的全部字段

HTML元素集合處於一種「實時的狀態」,這意味着當底層文檔對象更新時,它也會自動更新,也就是說,HTML元素集合與底層的文檔對象之間保持的鏈接。正因如此,每當你想從HTML元素集合中獲取一些信息時,都會產生一次查詢操做,這正是低效之源。

昂貴的集合

//這是一個死循環  //無論你信不信,反正我是信了  var alldivs = document.getElementsByTagName('div');  for (var i = 0; i < alldivs.length; i++){      document.body.appendChild(document.createElement('div'));   }

乍一看,這段代碼只是單純的把頁面中的div數量翻倍:遍歷全部的div,每次建立一個新的div並建立到添加到body中。
但事實上,這是一個死循環:由於循環的退出條件alldivs.length在每一次循環結束後都會增長,由於這個HTML元素集合反映的是底層文檔元素的實時狀態。
接下來,咱們經過這段代碼,對一個HTML元素集合作一些處理:

function toArray(coll){      for (var i = 0, a = [], len = coll.lengthl i < len; i++){          a[i] = coll[i];      }      return a;   }    //將一個HTML元素集合拷貝到一個數組中  var coll = document.getElementsByTagName('div');  var arr = toArray(coll);

如今比較如下兩個函數:

function loopCollection(){      for (var count = 0; count < coll.length; count++){          //processing...      }   }    function loopCopiedArray(){      for (var count = 0; count < arr.length; count++){          //processing...      }   }

在IE6中,後者比前者快114倍;IE7中119倍;IE8中79倍...
因此,在相同的內容和數量下,遍歷一個數組的速度明顯快於遍歷一個HTML元素集合。
因爲在每一次迭代循環中,讀取元素集合的length屬性會引起集合進行更新,這在全部的瀏覽器中都有明顯的性能問題,因此你也能夠這麼幹:

function loopCacheLengthCollection(){      var coll = document.getElementsByTagName('div'),          len = coll.length;      for (var count = 0; count < len; count++){          //processing...      }   }

這個函數和上面的loopCopiedArray()同樣快。

訪問集合元素時使用局部變量

通常來講,對於任何類型的DOM訪問,當同一個DOM屬性或者方法須要被屢次訪問時,最好使用一個局部變量緩存此成員。當遍歷一個集合時,首要優化原則是把集合存儲在局部變量中,並把length緩存在循環外部,而後使用局部變量訪問這些須要屢次訪問的元素。
一個栗子,在循環之中訪問每一個元素的三個屬性。

function collectionGlobal(){      var coll = document.getElementsByTagName('div'),          len = coll.length,          name = '';      for (var count = 0; count < len; count++){          name = document.getElementsByTagName('div')[count].nodeName;          name = document.getElementsByTagName('div')[count].nodeType;          name = document.getElementsByTagName('div')[count].tagName;          //個人天不會有人真的這麼寫吧...      }      return name;   }

上面這段代碼,你們不要當真...正常人確定是寫不出來的...這裏是爲了對比一下,因此把這種最慢的狀況寫給你們看。
接下來,是一個稍微優化了的版本:

function collectionLocal(){      var coll = document.getElementsByTagName('div'),          len = coll.length,          name = '';      for (var count = 0; count < length; count++){          name = coll[count].nodeName;          name = coll[count].nodeType;          name = coll[count].tagName;      }      return name;   }

此次就看起來正常不少了,最後是此次優化之旅的最終版本:

function collectionNodesLocal(){      var coll = document.getElementsByTagName('div'),          len = coll.length,          name = '',          ele = null;      for (var count = 0; count < len; count++){          ele = coll[count];          name = ele.nodeName;          name = ele.nodeType;          name = ele.tagName;      }      return name;   }

遍歷DOM

在DOM中爬行

一般你須要從某一個DOM元素開始,操做周圍的元素,或者遞歸查找全部的子節點。
考慮下面兩個等價的栗子:

//1  function testNextSibling(){      var el = document.getElementById('mydiv'),          ch = el.firstChild,          name = '';      do {          name = ch.nodeName;      } while (ch = ch.nextSibling);      return name;   }    //2  function testChildNodes(){      var el = document.getElementById('mydiv'),          ch = el.childNodes,          len = ch.length,          //childNodes是一個元素集合,所以在循環中主席緩存length屬性以免迭代更新          name = '';      for (var count = 0; count < len; count++){          name = ch[count].nodeName;      }      return name;   }

在不一樣瀏覽器中,兩種方法的運行時間幾乎相等。但在老版本的IE瀏覽器中,nextSibling的性能比childNodes更好一些。

元素節點

咱們知道,DOM節點有如下五種分類:

  • 整個文檔是一個文檔節點

  • 每一個HTML元素是元素節點

  • HTML元素內的文本是文本節點

  • 每一個HTML屬性是屬性節點

  • 註釋是註釋節點

諸如childNodes、firstChild、nextSibling這些DOM屬性是不區分元素節點和其餘類型的節點的,但每每咱們只須要訪問元素節點,此時須要作一些過濾的工做。事實上,這些類型檢查的過程都是沒必要要的DOM操做。
許多現代瀏覽器提供的API只返回元素節點,若是可用的話推薦直接只用這些API,由於它們的執行效率比本身在js中過濾的效率要高。

  1. 現代瀏覽器提供的API(被替換的API)

  2. children(childNodes)

  3. childElementCount (childNodes.length)

  4. firstElementChild (firstChild)

  5. lastElementChild (lastChild)

  6. nextElementSibling (nextSibling)

  7. previousElementSibling (previousSibling)

使用這些新的API,能夠直接獲取到元素節點,也正是所以,其速度也更快。

選擇器API

有時候爲了獲得須要的元素列表,開發人員不得不組合調用getElementById、getElementsByTagName,並遍歷返回的節點,但這種繁密的過程效率低下。
最新的瀏覽器提供了一個傳遞參數爲CSS選擇器的名爲querySelectorAll()的原生DOM方法。這種方式天然比使用js和DOM來遍歷查找元素要快的多。
好比,

var elements = document.querySelectorAll('#menu a');

這一段代碼,返回的是一個NodeList————包含着匹配節點的類數組對象。與以前不一樣的是,這個方法不會返回HTML元素集合,所以返回的節點不會對應實時的文檔結構,也避免了以前因爲HTML集合引發的性能(潛在邏輯)問題。
若是不使用querySelectorAll(),咱們須要這樣寫:

var elements = document.getElementById('menu').getElementsByTagName('a');

不只寫起來更麻煩了,更要注意的是,此時的elements是一個HTML元素集合,因此還須要把它copy到數組中,才能獲得一個與前者類似的靜態列表。
還有一個querySelector()方法,用來獲取第一個匹配的節點。

重繪與重排(Repaints & Reflows)

瀏覽器用來顯示頁面的全部「組件」,有:HTML標籤、js、css、圖片——以後會解析並生成兩個內部的數據結構:

  • DOM樹(表示頁面結構)

  • 渲染樹(表示DOM節點應該如何表示)

DOM樹中的每個須要顯示的節點在渲染樹中至少存在一個對應的節點。
渲染樹中的節點被稱爲「幀(frames)」或「盒(boxes)」,符合css盒模型的定義,理解頁面元素爲一個具備padding、margin、borders和position的盒子。
一旦渲染樹構建完成,瀏覽器就開始顯示頁面元素,這個過程稱爲繪製(paint)。

當DOM的變化影響了元素的幾何屬性(寬、高)——好比改變改變了邊框的寬度或者給一個段落增長一些文字致使其行數的增長——瀏覽器就須要從新計算元素的幾何屬性,一樣,頁面中其餘元素的幾何屬性和位置也會所以受到影響。
瀏覽器會使渲染樹中收到影響的部分消失,從新構建渲染樹,這個過程稱爲「重排(reflow)」。重排完成以後,瀏覽器會從新將受到影響的部分繪製到瀏覽器中,這個過程稱之爲「重繪(repaint)」。

若是改變的不是元素的幾何屬性,如:改變元素的背景顏色,不會發生重排,只會發生一次重繪,由於元素的佈局並無改變。
無論是重繪仍是重排,都是代價昂貴的操做,它們會致使web應用程序的UI反應遲鈍,應當儘量的減小這類過程的發生。

重排什麼時候發生?

  • 添加或刪除可見的DOM元素

  • 元素位置的改變

  • 元素尺寸的改變(padding、margin、border、height、width)

  • 內容改變(文本改變或圖片尺寸改變)

  • 頁面渲染器初始化

  • 瀏覽器窗口尺寸改變

  • 滾動條的出現(會觸發整個頁面的重排)

最小化重繪和重排

改變樣式

一個栗子:

var el = document.getElementById('mydiv');   el.style.borderLeft = '1px';   el.style.borderRight = '2px';   el.style.padding = '5px';

示例中,元素的三個樣式被改變,並且每個都會影響元素的幾何結構。在最糟糕的狀況下,這段代碼會觸發三次重排(大部分現代瀏覽器爲此作了優化,只會觸發一次重排)。從另外一個角度看,這段代碼四次訪問DOM,能夠被優化。

var el = document.getElementById('mydiv');  //思路:合併全部改變而後一次性處理  //method_1:使用cssText屬性  el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px';    //method_2:修改類名  el.className = 'anotherClass';

批量修改DOM

當你須要對DOM元素進行一系列操做的時候,不妨按照以下步驟:

  1. 使元素脫離文檔流

  2. 對其應用多重改變

  3. 把元素帶回文檔中

上面的這一套組合拳中,第一步和第三部分別會觸發一次重排。可是若是你忽略了這兩個步驟,那麼在第二步所產生的任何修改都會觸發一次重排。

在此安利三種可使DOM元素脫離文檔流的方法:

  • 隱藏元素

  • 使用文檔片斷(document fragment)在當前DOM以外構建一個子樹,再把它拷貝迴文檔

  • 將原始元素拷貝到一個脫離文檔的節點中,修改副本,完成後再替換原始元素

讓動畫元素脫離文檔流

通常狀況下,重排隻影響渲染樹中的一小部分,但也可能影響很大的一部分,甚至是整個渲染樹。
瀏覽器所需的重排次數越少,應用程序的響應速度也就越快。
想象這樣一種狀況,頁面的底部有一個動畫,會推移頁面整個餘下的部分,這將是一次代價昂貴的大規模重排!用戶也勢必會感受到頁面一卡一卡的。
所以,使用如下步驟能夠避免頁面中的大部分重排:

  1. 使用絕對定位讓頁面上的動畫元素脫離文檔流

  2. 動畫展現階段

  3. 動畫結束時,將元素恢復定位。

IE的:hover

從IE7開始,IE容許在任何元素上使用:hover這個css選擇器。
然而,若是你有大量元素使用了:hover,你會發現,賊喇慢!

事件委託(Event Delegation)

這一個優化手段也是在前端求職面試中的高頻題目。
當頁面中有大量的元素,而且這些元素都須要綁定事件處理器。
每綁定一個事件處理器都是有代價的,要麼加劇了頁面負擔,要麼增長了運行期的執行時間。再者,事件綁定會佔用處理時間,並且瀏覽器須要跟蹤每一個事件處理器,這也會佔用更多的內存。還有一種狀況就是,當這些工做結束時,這些事件處理器中的絕大多數都是再也不須要的(並非100%的按鈕或連接都會被用戶點擊),所以有不少工做是沒有必要的。
事件委託的原理很簡單——事件逐層冒泡並能被父級元素捕獲。
使用事件委託,只須要給外層元素綁定一個處理器,就能夠處理在其子元素上觸發的全部事件。
有如下幾點須要注意:

  • 訪問事件對象,判斷事件源

  • 按需取消文檔樹中的冒泡

  • 按需阻止默認動做

小結

訪問和操做DOM須要穿越鏈接ECMAScript和DOM兩個島嶼之間的橋樑,爲了儘量的減小「過橋費」,有如下幾點須要注意:

    • 最小化DOM訪問次數

    • 對於須要屢次訪問的DOM節點,使用局部變量存儲其引用

    • 若是要操做一個HTML元素集合,建議把它拷貝到一個數組中

    • 使用速度更快的API:好比querySelectorAll

    • 留意重排和重繪的次數

    • 事件委託

相關文章
相關標籤/搜索