jQuery.unique()的实现方式
jQuery 中的 unique()
jQuery中 的 unique() 函数实现了对 DOM ELement按出现位置进行排序并去除重复元素的功能。使用方法如下:
<html> <head></head> <body onload="test()"> <div id="div_1"> <div id="div_2" /> </div> <div id="div_3" /> <script type='text/javascript' src='jquery.js'></script> <script type='text/javascript'> function test() { var divs = [ document.getElementById('div_3'), document.getElementById('div_1'), document.getElementById('div_2'), document.getElementById('div_2'), document.getElementById('div_1') ]; var html = show('before uinque', divs); $.unique(divs); html += show('after uinque', divs); document.write(html); } function show(name, divs) { var html = ''; for (var i = 0; i < divs.length; ++i) { html += divs[i].getAttribute('id') + '<br />'; } return name + ':<br />' + html + '<br />'; } </script> </body> </html>
显示结果为:
before uinque: div_3 div_1 div_2 div_2 div_1 after uinque: div_1 div_2 div_3
从函数名来看,应该主要是用于去除重复元素的,为什么同时又先对 DOM Element 进行排序?为了解释这个问题,先让我们试一下不事先进行排序的情况下需要如何去除重复。
不排序的去除重复元素的实现
这里给出两种实现:
$ = function() { return { unique: function(elems) { for (var i = 0; i < elems.length; ++i) { for (var j = i + 1; j < elems.length; ++j) { if (elems[i] === elems[j]) { elems.splice(i--, 1); break; } } } return elems; }, unique2: function(elemsWithId) { var obj = {}; for (var i = 0; i < elemsWithId.length; ++i) { var elem = elemsWithId[i]; obj[elem.getAttribute('id')] = elem; } elemsWithId.length = 0; for (var id in obj) { elemsWithId.push(obj[id]) } return elemsWithId; } } }();
实现一使用了两重循环,算法复杂度为O(n^2);实现思路比较直观,即遍历数组,看每个元素是否与后面的元素重复,有重复则移除;但当DOM Element数量较多时性能较差,而jQuery中对大量元素进行去除重复的操作是很普遍的。
实现二将 Object 当做 HashMap/HashSet 来使用,算法复杂度为O(n);遗憾的是JavaScript中无法直接用 DOM ELement 作为 Object 的 key ,因此只能将 id 作为 key ,然而并非所有的 DOM Element 都是有 id 的,所以这种方法并不通用。而自己实现一个高性能的 HashSet(还需要自己动手计算 DOM Element 的 Hash Code ),工作量又比较大。
我们知道,基于比较的排序算法最多可以将算法复杂度降到O(nlgn),(比如结合使用快速排序和插入排序),之后遍历数组时只要比较相邻元素就可以了:
unique3: function(sortedElems) { for ( var i = 1; i < sortedElems.length; i++ ) { if ( sortedElems[i] === sortedElems[ i - 1 ] ) { sortedElems.splice( i--, 1 ); } } return sortedElems; }
JavaScript中又有内置的排序算法。因此,在JavaScript中,先排序后去除重复是较好的做法。
先排序后去除重复
先排序后去除重复的实现方式如下:
$ = function() { var sort = function(a, b){ var aParents = getParents(a), bParents = getParents(b), al = aParents.length, bl = bParents.length, i, ap, bp; for (i = 0; i < al && i < bl; i++) { ap = aParents[i]; bp = bParents[i]; if (ap !== bp) { return siblingCheck(ap, bp); } } return i === al ? siblingCheck(a, bp, -1) : siblingCheck(ap, b, 1); }; var getParents = function(elem) { var ret = [], cur; for (cur = elem; cur; cur = cur.parentNode) { ret.unshift(cur); } return ret; } var siblingCheck = function(a, b, ret) { if (a === b) { return ret; } var cur = a; while (cur && (cur = cur.nextSibling)) { if (cur === b) { return -1; } } return 1; }; return { unique : function(elems) { elems.sort(sort); for ( var i = 1; i < elems.length; i++ ) { if ( elems[i] === elems[ i - 1 ] ) { elems.splice( i--, 1 ); } } return elems; } } }();
这里使用了 Array 的 sort(...) 方法,并传入自定义的排序函数(sort函数)。
sort函数的做法是获取两个被比较元素的所有“直系祖宗”,从而确定了两个元素在 DOM 树中的位置;一般来说,两个元素有共同的根,那么就从根元素开始依次向下遍历,直到“分叉点”,再对分叉点的元素进行比较。如果直到遍历结束,仍未能达到分叉点(如元素a先遍历完,那么 i 就与 al 相等了),则直接将 a 与 b 当前遍历到的“祖宗”进行比较。
排序时检查重复
接下来,我们针对这样的情况进行优化:假设元素中不存在重复,那么我们就可以不执行后面的遍历并去除重复的操作了。实现如下:
$ = function() { var hasDuplicate = true; [0, 0].sort(function() { hasDuplicate = false; return 0; }); var sort = function(a, b){ if (a === b) { hasDuplicate = true; return 0; } // Other Codes ... }; var getParents = function(elem) { // Other Codes ... } var siblingCheck = function(a, b, ret) { // Other Codes ... }; return { unique : function(elems) { elems.sort(sort); if (hasDuplicate) { for ( var i = 1; i < elems.length; i++ ) { if ( elems[i] === elems[ i - 1 ] ) { elems.splice( i--, 1 ); } } } return elems; } } }();
一个特殊的例外是某些浏览器可能进行了特殊的优化,那么在元素相等时可能就没有调用我们的排序函数了;这种情况下,排序时检查重复的方案就不可行。因此,如果浏览器在元素相等的情况下会调用我们的排序函数,那么就将 hasDuplicate 置为 false,并在排序过程中检查重复;否则,无论如何都视为存在重复,仍然进行遍历去除重复的操作。
使用compareDocumentPosition()进行优化
事实上,除 IE 之外浏览器都有内置的 compareDocumentPosition() 方法,用于比较两个 DOM Element 的位置,因此我们可以引入 compareDocumentPosition() 进行优化:
if (document.documentElement.compareDocumentPosition) { var sort = function(a, b) { if (a === b) { hasDuplicate = true; return 0; } if (!a.compareDocumentPosition || !b.compareDocumentPosition) { return a.compareDocumentPosition ? -1 : 1; } return a.compareDocumentPosition(b) & 4 ? -1 : 1; } } else { var sort = function(a, b) { // Original implemention ... } }
a.compareDocumentPosition(b) & 4 表示取返回值的二进制表示的倒数第三位;compareDocumentPosition() 的返回值的每个二进制位表示不同的含义,其中倒数第三位表示元素 a 是否在元素 b 的 “前面”,这正是我们需要的。
最后的优化
最后,在sort的原始实现前插入一些代码,使得该函数有机会提前退出:
var sort = function(a, b){ var aParents = getParents(a), bParents = getParents(b), al = aParents.length, bl = bParents.length, ap = a.parentNode, bp = b.parentNode, i; if (a === b) { hasDuplicate = true; return 0; // If the nodes are siblings (or identical) we can do a quick check } else if (ap === bp) { return siblingCheck( a, b ); // If no parents were found then the nodes are disconnected } else if (!ap) { return -1; } else if (!bp) { return 1; } for (i = 0; i < al && i < bl; i++) { ap = aParents[i]; bp = bParents[i]; if (ap !== bp) { return siblingCheck(ap, bp); } } return i === al ? siblingCheck(a, bp, -1) : siblingCheck(ap, b, 1); };
以上就是 jQuery.unique() 的实现过程了。jQuery 的 unique() 在去除重复前先进行了排序,并使用了多种优化手段,实现了性能较好并且比较通用的去除重复的 DOM Element 的功能。
附件是本文中用到的一些代码。