JavaScript框架设计
上QQ阅读APP看书,第一时间看更新

6.4.3 节点排序与去重

为了让选择器引擎搜到的结果集尽可能接近原生 API 的结果(因为在最新的浏览器中,我们可能只使用querySelectorAll实现),我们需要让元素节点按它们在DOM树出现的顺序排序。

在IE及Opera早期的版本,我们可以使用sourceIndex进行排序。

标准浏览器可以使用 compareDocumentPosition,上面不是介绍它可以判定两个节点的位置关系吗?我们只要将它们的结果按位与4不等于0就知道其前后顺序了。

此外,标准浏览器的Range对象有一个compareBoundaryPoints方法,它也能迅速得到两个元素的前后顺序。

      var compare = comparerange.compareBoundaryPoints(how, sourceRange);

compare:返回1, 0, −1(0为相等,1为comparerange在sourceRange之后,−1为comparerange在sourceRange之前)。

how:比较哪些边界点,为常数。

· Range.START_TO_START——比较两个Range节点的开始点。

· Range.END_TO_END——比较两个Range节点的结束点。

· Range.START_TO_END——用sourceRange的开始点与当前范围的结束点比较。

· Range.END_TO_START——用sourceRange的结束点与当前范围的开始点比较。

特别的情况发生于要兼容旧版本标准浏览器与XML文档时,这时只有一些很基础的DOM API,我们需要使用 nextSibling 来判定谁是“哥哥”,谁是“弟弟”。如果它们不是同一个父节点,我们就需要将问题转化为求最近公共祖先,判定谁是“父亲”,谁是“伯父”。到这里,已经是纯算法的问题了。实现的思路有许多,最直观也最笨的做法是,不断向上获取它们的父节点,直到HTML元素,连同最初的那个节点,组成两个数组,然后每次取数组最后的元素进行比较,如果相同就去掉,一直去掉到不相同为止,最后用nextSibling结束。下面是测试代码,自己找一个HTML页面做标本。

      window.onload = function() {
          function shuffle(a) {
              //洗牌
              var array = a.concat();
              var i = array.length;
              while (i) {
                  var j = Math.floor(Math.random() * i);
                  var t = array[--i];
                  array[i] = array[j];
                  array[j] = t;
              }
              return array;
          }
          var log = function(s) {
              //查看调试消息
              window.console && window.console.log(s)
          }
          var sliceNodes = function(arr) {
              //将NodeList转换为纯数组
              var ret = [],
                      i = arr.length;
              while (i)
                  ret[--i] = arr[i];
              return ret;
          }
          var sortNodes = function(a, b) {
              //节点排序
              var p = "parentNode",
                      ap = a[p],
                      bp = b[p];
              if (a === b) {
                  return 0
              } else if (ap === bp) {
                  while (a = a.nextSibling) {
                      if (a === b) {
                          return -1
                      }
                  }
                  return 1
              } else if (!ap) {
                  return -1
              } else if (!bp) {
                  return 1
              }
              var al = [],
                      ap = a
              //不断往上取,一直取到HTML
              while (ap && ap.nodeType === 1) {
                  al[al.length] = ap
                  ap = ap[p]
              }
              var bl = [],
                      bp = b;
              while (bp && bp.nodeType === 1) {
                  bl[bl.length] = bp
                  bp = bp[p]
              }
              //然后逐一去掉公共祖先
              ap = al.pop();
              bp = bl.pop();
              while (ap === bp) {
                  ap = al.pop();
                  bp = bl.pop();
              }
              if (ap && bp) {
                  while (ap = ap.nextSibling) {
                      if (ap === bp) {
                          return -1
                      }
                  }
                  return 1
              }
              return ap ? 1 : -1
          }
          var els = document.getElementsByTagName("*")
          els = sliceNodes(els);       //转换成纯数组
          log(els);
          els = shuffle(els);          //洗牌(模拟选择器引擎最初得到的结果集的情况)
          log(els);
          els = els.sort(sortNodes); //进行节点排序
          log(els);
      }

此外,我们还有一种方法,就是选择结束后,用document.getElementsByTagName(“*”)得到所有元素节点,这时它们肯定是排好序的。我们再依次为它们添加一个类似sourceIndex的自定义属性,值为它的索引值,接下来怎么做就无需我多言了。

好了,我们暂且放下,看一下各大浏览器的实现。

Mootools的Slick引擎,它的比较函数已经注明是来自Sizzle的(准确来说Sizzle1.6左右,现在它也被改得面目全非)。

      features.documentSorter = (root.compareDocumentPosition) ? function(a, b) {
          if (!a.compareDocumentPosition || !b.compareDocumentPosition)
              return 0;
          return a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1;
      } : ('sourceIndex' in root) ? function(a, b) {
          if (!a.sourceIndex || !b.sourceIndex)
              return 0;
          return a.sourceIndex - b.sourceIndex;
      } : (document.createRange) ? function(a, b) {
          if (!a.ownerDocument || !b.ownerDocument)
              return 0;
          var aRange = a.ownerDocument.createRange(),
                  bRange = b.ownerDocument.createRange();
          aRange.setStart(a, 0);
          aRange.setEnd(a, 0);
          bRange.setStart(b, 0);
          bRange.setEnd(b, 0);
          return aRange.compareBoundaryPoints(Range.START_TO_END, bRange);
      } : null;

它没有打算支持XML与旧版本标准浏览器,不支持就不排序。

mass Framework 的 Icarus 引擎,它结合了一位编程高手 JK 给出的算法,在排序去重上远胜Sizzle。突破点在于,无论Sizzle或者Slick,它们都是通过传入比较函数进行排序。而数组的原生sort 方法,当它传一个比较函数时,不管它内部用哪种排序算法ecma没有规定sort的具体实现,因此浏览器各显神器,比如Firefox2使用堆排序,Firefox3使用归并排序,IE速度较慢,具体算法不明,可能为冒泡或者插入排序,而Chrome则为了最大效率,采用了两种算法:chrome use quick sort for a large dataset ( >22) ,and for a smaller dataset (<=22) chrome use insert sort but modified to use binary search to find insert point ,so traditionally insert sort is stable ,but chrome make it faster and unstable , in conclusion chrome’s sort is quick and unstable .资料来自:http://yiminghe.iteye.com/blog/469713,都需要多次比对,所以非常耗时。如果能设计让排序在不传参的情况进行,那么速度就会提高N倍!

下面是具体思路(当然只能用于IE或早期Opera)。

(1)取出元素节点的sourceIndex值,转换成一个String对象;

(2)将元素节点附在String对象上;

(3)用String对象组成数组;

(4)用原生的sort进String对象数组排序;

(5)在排好序的String数组中,按序取出元素节点,即可得到排好序的结果集。

      function unique(nodes) {
          if (nodes.length < 2) {
              return nodes;
          }
          var result = [],
                  array = [],
                  uniqResult = {},
                  node = nodes[0],
                  index, ri = 0,
                  sourceIndex = typeof node.sourceIndex === "number",
                  compare = typeof node.compareDocumentPosition == "function";
      //如果支持sourceIndex,我们将使用更为高效的节点排序
      //http://www.cnblogs.com/jkisjk/archive/2011/01/28/array_quickly_sortby.html
          if (!sourceIndex && !compare) { //用于旧版本IE的XML
              var all = (node.ownerDocument || node).geElementsByTagName("*");
              for (var index = 0; node = all[index]; index++) {
                  node.setAttribute("sourceIndex", index);
              }
              sourceIndex = true;
          }
          if (sourceIndex) { //IE opera
              for (var i = 0, n = nodes.length; i < n; i++) {
                  node = nodes[i];
                  index = (node.sourceIndex || node.getAttribute("sourceIndex")) + 1e8;
                  if (!uniqResult[index]) {      //去重
                      (array[ri++] = new String(index))._ = node;
                      uniqResult[index] = 1;
                  }
              }
              array.sort();                      //排序
              while (ri)
                  result[--ri] = array[ri]._;
              return result;
          } else {
              nodes.sort(sortOrder);             //排序
              if (sortOrder.hasDuplicate) {     //去重
                  for (i = 1; i < nodes.length; i++) {
                      if (nodes[i] === nodes[i - 1]) {
                          nodes.splice(i--, 1);
                      }
                  }
              }
              sortOrder.hasDuplicate = false;   //还原
              return nodes;
          }
      }
      function sortOrder(a, b) {
          if (a === b) {
              sortOrder.hasDuplicate = true;
              return 0;
          } //现在标准浏览器的HTML与XML好像都支持compareDocumentPosition
          if (!a.compareDocumentPosition || !b.compareDocumentPosition) {
              return a.compareDocumentPosition ? -1 : 1;
          }
          return a.compareDocumentPosition(b) & 4 ? -1 : 1;
      }