按照DOM中元素的位置排序包含DOM元素的数组

14

背景

我正在开发一个jQuery插件,将DOM元素以数组的形式进行存储,这样可以在不使用速度较慢的data()的情况下,在这些元素旁边存储更多信息。

该数组如下所示:

[
    { element: DOMElement3, additionalData1: …, additionalData2: … },
    { element: DOMElement1, additionalData1: …, additionalData2: … },
    { element: DOMElement2, additionalData1: …, additionalData2: … },
]

这个插件的工作方式使得我无法按照可预测的顺序将这些元素推送到数组中,这意味着DOMElement3实际上可能会出现在比DOMElement2更低的索引位置。

然而,我需要这些数组元素按照它们包含的DOM元素在DOM中出现的顺序排序。一旦排序完成,前面例子中的数组将如下所示:

[
    { element: DOMElement1, additionalData1: …, additionalData2: … },
    { element: DOMElement2, additionalData1: …, additionalData2: … },
    { element: DOMElement3, additionalData1: …, additionalData2: … },
]

当然,这是在假设 DOMElement1 出现在 DOMElement2 之前,DOMElement2 出现在 DOMElement3 之前的情况下。

被放弃的解决方案

add() 方法返回一组DOM元素,它们按照在DOM中出现的顺序排列。我本可以使用它,但要求我使用一个 jQuery 集合——这意味着我必须重构上述插件的大部分内容以使用不同的存储格式。这就是为什么我认为这是最后的解决方案。

还有其他解决方案吗?

我原本想象使用 map() 和每个 DOM 元素绑定的全局 DOM 索引来完成,但似乎没有这样的“全局 DOM 索引”。

你能想到什么方法?(如果编写代码,欢迎使用原生JS和jQuery)


我很好奇你是否已经对你的方法与“数据”进行了基准测试,以查看你的方法有多快。 - James Montagne
这个插件的工作方式阻止我按可预测的顺序将这些元素推入数组中。看起来你应该修复它。 - A. Wolff
@JamesMontagne 我没有尝试过,但是将元素推入数组似乎比使用jQuery的内部缓存来附加额外数据到每个元素要快。我会立即创建一个JSperf来对此进行基准测试,并在这里发布结果。 - user1728278
@A.Wolff 插件允许用户随时向该数组添加元素。因此,如果仍然使用相同的示例,某人首先将 DOMElement3 添加到数组中,然后是 DOMElement1DOMElement2,则该数组将与第一个代码示例完全相同。 - user1728278
@JamesMontagne 看起来我的方法确实比使用“data()”更快,至少在以下测试用例中:http://jsperf.com/storing-data-next-to-dom-elements - user1728278
4个回答

25

有一个非常有用的函数叫做compareDocumentPosition,它会返回一个数字,根据两个元素相对位置来确定。可以在.sort回调中使用它:

yourArray.sort(function(a,b) {
    if( a === b) return 0;
    if( !a.compareDocumentPosition) {
        // support for IE8 and below
        return a.sourceIndex - b.sourceIndex;
    }
    if( a.compareDocumentPosition(b) & 2) {
        // b comes before a
        return 1;
    }
    return -1;
});

1
遗憾的是,IE8仍然是一款重要的现实浏览器,并且没有compareDocumentPosition方法。你必须使用contains方法。更多信息请参见John Resig的这篇文章 - T.J. Crowder
@A.Wolff:那个封装和查找的成本有多高? - T.J. Crowder
非常棒的讨论,感谢提供所有这些链接。由于在此处不支持IE8,我将不得不使用Resig的代码片段使其在所有主要浏览器中运行。我还想出了另一个类似原始JavaScript的解决方案,并正在编写这3种解决方案之间的性能比较。完成后会在这里发布。 - user1728278
1
以下是我测试过的三种解决方案,它们分别在以下链接中:compareDocumentPosition(@NiettheDarkAbsol的答案),remapping with expando(@T.J.Crowder的答案)和document.all indexes(我尝试解决该问题的另一种方法)。最后,这里是比较所有三种方法的jsPerf链接:http://jsperf.com/sort-array-containing-dom-elements-according - user1728278
1
举起双手,我赢了!大胜利!哇! - Niet the Dark Absol
显示剩余9条评论

1

虽然我不建议您采用这种方法,但使用jquery可以获得“全局DOM索引”:

var $all = $("*");

// use index to get your element's index within the entire DOM
$all.index($YOUR_EL);

哈哈,那确实可以,谢谢,最后的备选方案 #2! - user1728278

1

您不需要进行大量的重构即可利用jQuery的排序功能。您可以将其用作临时排序机制。以下是一个即兴示例:

function putInDocumentOrder(a) {
      var elements;

    // Get elements in order and remember the index of
    // the entry in `a`
    elements = $().add(a.map(function(entry, index){
        entry.element.__index = index;
        return entry.element;
    }));

    // Build array of entries in element order
    a = elements.map(function(){
        return a[this.__index];
    }).get();

    return a;
}

这需要一个expando,但它可以完成工作。实时示例

这样做的好处是它适用于jQuery支持的所有浏览器,而不是你自己处理边缘情况(例如IE8不支持compareDocumentPosition)。


这个解决方案适用于 add()$("div").add([DOMElement3, DOMElement2, DOMElement1]) 将会得到以下 jQuery 集合:[DOMElement1, DOMElement2, DOMElement3]。然而,我不明白这与我所写的“被废弃的解决方案”有何不同。 - user1728278
@user1728278:你说你想使用一个数组,而不是一个jQuery集合。区别在于,通过最后使用get(),你最终得到的是一个数组,而不是一个jQuery集合。 - T.J. Crowder
@T.J.Crowder 这是正确的。另一个要求是,正如代码示例所示,数组不包含DOM元素,而是包含DOM元素(以及其他内容)的对象;如果我没有表达清楚,很抱歉。恐怕当数组成员不仅仅是DOM元素时,这个解决方案将无法生效。 - user1728278
@user1728278:啊,好的。我有一个关于那个的想法,等一下。 - T.J. Crowder
1
最终,您可能需要在排序集合和数据缓存之间建立某种映射。这就是我第一个问题背后的原因,即您是否对其进行了基准测试。如果最终必须完全重新创建“data”的功能,则应该只使用“data”。 - James Montagne
显示剩余6条评论

0
如果您可以假设页面上的可见位置相同,那么这可能是一个相对快速的解决方案:
function compareByVisibleLocation(a, b) {
  if (a.offsetTop > b.offsetTop) {
    return 1;
  } else if (a.offsetTop < b.offsetTop) {
    return -1;
  }

  if (a.offsetLeft > b.offsetLeft) {
    return 1;
  } else if (a.offsetLeft < b.offsetLeft) {
    return -1;
  } else {
    return 0;
  }
}

nodes.sort(compareByVisibleLocation);

否则,您可以使用querySelectorAll()快速构建索引(它是深度优先的前序遍历),通过显式地使用indexed-node类名标记要索引的节点。
var nodes = document.querySelectorAll('.indexed-node');

或者,如果那样更好,通过获取一个单一节点类型的所有内容:

var nodes = document.querySelectorAll('div');

或者通过获取所有节点:

var nodes = document.querySelectorAll('*');

然后为每个节点添加一个索引属性:

for (var i = 0; i < nodes.length; i++) { nodes[i].__DomIndex = i; }

你可以这样排序:

var nodes = yourGetMethod().sort(function(a,b) {
  if (a === b) {
    return 0;
  } else if (a.__DomIndex > b.__DomIndex) {
    return 1;
  } else {
    return -1;
  }
});

应该可以兼容IE8及以下版本。


视觉位置和DOM顺序经常无关。浮动,不同的定位等则成为问题所在。 - T.J. Crowder
@T.J.Crowder 我当然不反对。但是,如果OP的意图是按照页面上出现的顺序填充某些内容,那么这可能正是他需要的... - svidgen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接