[翻译]加速 JavaScript:处理 DOM

当制作 RIA(Rich Internet Application) 时,我们使用 JavaScript 来改变或增加元素。这是靠 DOM (Document
Object Model 文档对象模型) 来完成的,这是如何影响 app 的速度呢?

处理 DOM 会使浏览器 reflow (浏览器决定如何显示东东的进程)。直接操作 DOM,改变元素的 CSS 样式,改变浏览器窗口的大小会触发 reflow。访问元素的布局属性(如offsetHeight 和 offsetWidth) 会触发 reflow。由于每次 reflow 需要时间,所有我们能减少 reflow 越多,app 越快。

处理 DOM 不仅操作页面中存在的元素还包括创建的元素。下面四个部分覆盖了 DOM 操作和 DOM generation,减少浏览器中 reflow 触发的次数。

CSS class 切换 DOM 操作

这个部分我们改变一个元素和它后代的多个样式属性,只触发一次回流。

问题

我们写一个函数来改变一个元素中的所有链接的 className 属性。可以通过简单遍历每个 a 并更新他们的 className 属性。

function selectAnchor(element) {  
  element.style.fontWeight = 'bold';
  element.style.textDecoration = 'none';
  element.style.color = '#000';
}

解决方法

我们可以增加一个设置所有样式属性的 class 来解决这个问题。通过给元素增加这个class,现在我们只触发一次浏览器 reflow。同时分离了表现和行为(译注:这个在我之前的《body标签class属性的妙用(Google Reader前端简单分析)》中也有提及)

.selectedAnchor {
  font-weight: bold;
  text-decoration: none;
  color: #000;
}

function selectAnchor(element) {  
  element.className = 'selectedAnchor';
}

Out-of-the-flow DOM 操作

这个部分我们创建多个元素并把它们插入 DOM 只触发一次 reflow。使用了叫做 DocumentFragment 的东东。我们在 DOM 外创建一个DocumentFragment (所以是 out-of-the-flow)。之后创建并添加进多个元素。最后,我们移动该 DocumentFragment 中的所有元素到 DOM,这只触发一次 reflow。

问题

让我们写一个改变一个元素中所遇链接的 className 属性的函数。通过简单的遍历每个 a 并更新他们的 href 属性。问题在于,每个 a 会触发一次reflow。

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);
    }
  };
}

现在我们用这个函数去更新一个元素中的所有链接,这 out-of-the-flow,并且在移除元素时和插入元素时只触发一次 reflow。

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 Generation

这个部分我们创建并添加一个元素至 DOM 中,仅触发一次 reflow。创建元素后,在新元素添加至 DOM 前做好所有的改变。

问题

写一个添加新a 元素至父元素的函数。这个函数让你为该链接提供 class 和 文本。我们可以创建元素,添加进 DOM,之后设置这些属性。这触发3次 reflow。

function addAnchor(parentElement, anchorText, anchorClass) {  
  var element = document.createElement('a');
  parentElement.appendChild(element);
  element.innerHTML = anchorText;
  element.className = anchorClass;
}

解决方法

最后插入这个子节点至 DOM。这触发一次 reflow。

function addAnchor(parentElement, anchorText, anchorClass) {  
  var element = document.createElement('a');
  element.innerHTML = anchorText;
  element.className = anchorClass;
  parentElement.appendChild(element);
}

尽管如此,如果我们要添加大量的链接至一个元素时这有一些问题。通过这个方法,每添加一个链接触发一次 reflow。下一部分解决这个问题。

DocumentFragment DOM Generation

这个部分,我们创建多个元素并把它们插入 DOM 触发一次 reflow。使用了一个叫做 DocumentFragment 的东东。我们在 DOM 外创建一个DocumentFragment (所以是 out-of-the-flow)。之后创建并添加多个元素进来。最后,把所有DocumentFragment 中的元素移至DOM 并只触发一次 reflow。

问题

写一个添加10个连接至一个元素的函数。如果只是简单的添加每个新链接至这个元素,会触发10次 reflow。

function addAnchors(element) {  
  var anchor;
  for (var i = 0; i < 10; i ++) {
    anchor = document.createElement('a');
    anchor.innerHTML = 'test';
    element.appendChild(anchor);
  }
}

解决方法

为了解决这个问题,我们创建一个 DocumentFragment 并添加每个新链接至此。当我们使用appendChild 添加 DocumentFragment至元素时,所有 DocumentFragment 的子节点 实际上添加进了这个元素中。这只触发一次 reflow。

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);
}