加速Javascript:DOM操作优化
原文:《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); }