原文:《Speeding up JavaScript: Working with the DOM》
作者: KeeKim Heng, Google Web Developer
翻譯:http://www.blogjava.net/emu/archive/2010/03/01/314185.html
在我們開發互連網富應用(RIA)時,我們經常寫一些javascript指令碼來修改或者增加頁面元素,這些工作最終是DOM——或者說文檔對象模 型——來完成的,而我們的實現方式會影響到應用的響應速度。
DOM操作會導致瀏覽器重解析(reflow),這是瀏覽器的一個決定頁面元素如何展現的計算過程。直接修改DOM,修改元素的CSS樣式,修改瀏 覽器的視窗大小,都會觸發重解析。讀取元素的布局屬性比如offsetHeithe或者offsetWidth也會觸發重解析。重解析需要花費計算時間, 因此重解析觸發的越少,應用就會越快。
DOM操作通常要不就是修改已經存在的頁面上的元素,要不就是建立新的頁面元素。下面的4種最佳化方案覆蓋了修改和建立DOM節點兩種方式,協助你減 少觸發瀏覽器重解析的次數。
方案一:通過CSS類名切換來修改DOM
這個方案讓我們可以一次性修改一個元素和它的子項目的多個樣式屬性而只觸發一次重解析。
需求:
(emu註:原文作者寫到這裡的時候腦子顯然短路了一下,把後面的Out-of-the-flow DOM Manipulation模式要解決的問題給擺到這裡來了,不過從示範代碼中很容易明白作者真正想描述的問題,因此emu就不照翻原文了)
我們現在需要寫一個函數來修改一個超連結的幾個樣式規則。要實現很簡單,把這幾個規則對應的屬性逐一改了就好了。但是帶來的問題是,每修改一個樣式 屬性,都會導致一次頁面的重解析。
function selectAnchor(element) { element.style.fontWeight = 'bold'; element.style.textDecoration = 'none'; element.style.color = '#000'; }
解決方案
要解決這個問題,我們可以先建立一個樣式名,並且把要修改的樣式規則都放到這個類名上,然後我們給超連結添加上這個新類名,就可以實現添加幾個樣式 規則而只觸發一次重解析了。這個模式還有個好處是也實現了表現和邏輯相分離。
.selectedAnchor { font-weight: bold; text-decoration: none; color: #000; } function selectAnchor(element) { element.className = 'selectedAnchor'; }
方案二:在非渲染區修改DOM
(emu註:作者在這裡再次腦子短路,把DocumentFragment DOM Generation模式的介紹提前到這裡來了,emu只好再次發揮一下)
上一個方案解決的是修改一個超連結的問題,當一次需要對很多個超連結進行相同修改的時候,這個方案就可以大顯身手了。
需求
需求是這樣的,我們要寫一個函數來修改一個指定元素的子項目中所有的超連結的樣式名(className)屬性。要實現很簡單,我們可以通過遍曆每 個超連結並且修改它們的樣式名來完成任務。但是帶來的問題就是,每修改一個超連結都會導致一次重解析。
function updateAllAnchors(element, anchorClass) { var anchors = element.getElementsByTagName('a'); for (var i = 0, length = anchors.length; i < length; i ++) { anchors[i].className = anchorClass; } }
解決方案
要解決這個問題,我們可以把被修改的指定元素從DOM裡面移除,再修改所有的超連結,然後在把這個元素插入回到它原來的位置上。為了完成這個複雜的 操作,我們可以先寫一個可重用的函數,它不但移除了這個DOM節點,還返回了一個把元素插回到原來的位置的函數。
/** * Remove an element and provide a function that inserts it into its original position * @param element {Element} The element to be temporarily removed * @return {Function} A function that inserts the element into its original position **/ function removeToInsertLater(element) { var parentNode = element.parentNode; var nextSibling = element.nextSibling; parentNode.removeChild(element); return function() { if (nextSibling) { parentNode.insertBefore(element, nextSibling); } else { parentNode.appendChild(element); } }; }
有了上面這個函數,現在我們就可以在一個不需要解析渲染的元素上面修改那些超連結了。這樣只在移除和插入元素的時候各觸發一次重解析。
function updateAllAnchors(element, anchorClass) { var insertFunction = removeToInsertLater(element); var anchors = element.getElementsByTagName('a'); for (var i = 0, length = anchors.length; i < length; i ++) { anchors[i].className = anchorClass; } insertFunction(); }
方案三:一次性的DOM元素產生
這個方案讓我們建立一個元素的過程只觸發一次重解析。在建立完元素以後,先進行所有需要的修改,最後才把它插入到DOM裡面去就可以了
需求
需求是這樣的,實現一個函數,往一個指定的父元素上插入一個超連結元素。這個函數要同時可以設定這個超連結的顯示文字和樣式類。我們可以這樣做:創 建元素,插入到DOM裡面,然後設定相應的屬性。這就要觸發3次重解析。
function addAnchor(parentElement, anchorText, anchorClass) { var element = document.createElement('a'); parentElement.appendChild(element); element.innerHTML = anchorText; element.className = anchorClass; }
解決方案
很簡單,我們只要把插入元素這個操作放到最後做,就可以只進行一次重解析了。
function addAnchor(parentElement, anchorText, anchorClass) { var element = document.createElement('a'); element.innerHTML = anchorText; element.className = anchorClass; parentElement.appendChild(element); }
不過,要是我們想要插入很多個超連結到一個元素裡面的話,那麼這個做法還是有問題:每插入一個超連結還是要觸發一次重解析。下一個方案可以解決這個 問題。
方案四:通過文檔片段對象(DocumentFragment)建立一組元素
這個方案允許我們建立並插入很多個元素而只觸發一次重解析。要實現這點需要用到所謂的文檔片段對象(DocumentFragment)。我們先在 DOM之外建立一個文檔片段對象(這樣它也就不需要解析和渲染),然後我們在文檔片段對象中建立很多個元素,最後我們把這個文檔片段對象中所有的元素一次 性放到DOM裡面去,只觸發一次重解析。
需求
我們要寫一個函數,往一個指定的元素上面增加10個超連結。如果我們簡單的直接插入10個超連結到元素上面,就會觸發10次重解析。
function addAnchors(element) { var anchor; for (var i = 0; i < 10; i ++) { anchor = document.createElement('a'); anchor.innerHTML = 'test'; element.appendChild(anchor); } }
解決方案
要解決這個問題,我們要先建立一個文檔片段對象,然後把每個新建立的超連結都插入到它裡面去。當我們把文檔片段對象用appendChild命令插 入到指定的節點時,這個文檔片段對象的所有子節點就一起被插入到指定的元素裡面,而且只需要觸發一次重解析。
function addAnchors(element) { var anchor, fragment = document.createDocumentFragment(); for (var i = 0; i < 10; i ++) { anchor = document.createElement('a'); anchor.innerHTML = 'test'; fragment.appendChild(anchor); } element.appendChild(fragment); }