如何循环遍历页面上的所有元素,包括伪元素?

11

如何循环遍历所有元素,包括伪元素?我知道可以使用getComputedStyle(element,pseudoEl)来获取其内容,但是我一直无法找到一种方法来获取页面上的所有伪元素,以便我可以使用前面提到的函数来获取它们的内容/样式。这似乎是一个简单的问题,但我一直找不到解决方案。


2
我的理解是所有元素都隐含有伪元素,它们的默认样式使它们不可见。 - zzzzBov
1
请看这个答案:https://dev59.com/J2445IYBdhLWcg3wGWd7#5041526 - Babblo
好的,你至少可以通过上述API选择样式,这已经“证明”了那个问题是错误的~而且那个问题已经有2年了,所以我暗自希望可能会发布一些新的API。 - David Mulder
好问题。我真的很讨厌伪元素的本质;为什么它们不应该对dom可见?如果我要编写规范,我会让它们实际上将内容插入到dom中。 - bjb568
3个回答

5
你在正确的方向上。使用getElementsByTagName("*")querySelectorAll("*")循环遍历所有DOM元素相当容易。然后,我们需要查看每个元素是否有伪元素,正如@zzzzBov所提到的那样。虽然你没有明确提及,但我认为你最感兴趣的是:before:after伪元素。因此,我们利用了一个事实,即必须使用content属性才能实际使用伪元素:我们只需简单地检查它是否设置。希望这个小脚本能帮到你:
var allElements = document.getElementsByTagName("*");

for (var i=0, max=allElements.length; i < max; i++) {
    var before = window.getComputedStyle(allElements[i], ':before');  
    var after = window.getComputedStyle(allElements[i], ':after'); 
    if(before.content){
        // found :before
        console.log(before.content);
    }
    if(after.content){
        // found :after
        console.log(after.content);
    }
}

哈哈,我已经有完全相同的代码了,等一下,我也要发布它,因为它的结构略微不同,可能对一些人有用。还是希望能看到一些更便宜的解决方案。 - David Mulder
顺便说一下,我也会在这个问题上设置赏金以寻求更好的答案,但除此之外,你可以期待一些50点额外声望的奖励 :P - David Mulder
不是所有的标签都可以有before/after内容,例如input。你可以通过只命中“可包含内容”的标签列表来减少命中所有标签的开销。更好的方法是使用一个短的自定义白名单,比如querySelectorAll("div,a,i")。你也可以从body/#main/form开始而不是从根开始,以跳过meta、title、link等。只是说一下... - dandavis
我注意到这个问题,想起来之前并没有完全正确地获取奖励,如果我不得已需要这个问题的解决方法,并且性能可接受,我可能会直接使用的答案。让我们来修正一下这种情况。 :) - Jordan Gray

5
经过一些性能测试,我的建议是:
  • 大多数情况下,请使用Max K的解决方案。在大多数情况下,性能已经足够好了,它是可靠的,并且代码行数不到15行(我的代码约为70行)。
  • 如果你确实需要挤出每一毫秒,而你又知道(因为你真的测试过)它更快,请使用下面的解决方案。

通常更快的解决方案

您已经知道如何使用document.querySelectorAll('*')获取文档中的所有元素列表。这在大多数情况下都有效,但对于仅有少数元素具有伪元素的大型文档来说,它可能会很慢。

在这种情况下,我们可以从不同的角度来解决这个问题。首先,我们遍历文档样式表并构建一个与beforeafter伪元素相关联的选择器字典:

function getPseudoElementSelectors() {
    var matchPseudoSelector = /:{1,2}(after|before)/,
        found = { before: [], after: [] };

    if (!(document.styleSheets && document.styleSheets.length)) return found;

    return Array.from(document.styleSheets)
        .reduce(function(pseudoSelectors, sheet) {
            try {
                if (!sheet.cssRules) return pseudoSelectors;

                // Get an array of all individual selectors.
                var ruleSelectors = Array.from(sheet.cssRules)
                    .reduce(function(selectors, rule) {
                        return (rule && rule.selectorText)
                            ? selectors.concat(rule.selectorText.split(','))
                            : selectors;
                    }, []);

                // Construct a dictionary of rules with pseudo-elements.
                var rulePseudoSelectors = ruleSelectors.reduce(function(selectors, selector) {

                    // Check if this selector has a pseudo-element.
                    if (matchPseudoSelector.test(selector)) {
                        var pseudoElement = matchPseudoSelector.exec(selector)[1],
                            cleanSelector = selector.replace(matchPseudoSelector, '').trim();

                        selectors[pseudoElement].push(cleanSelector);
                    }

                    return selectors;
                }, { before: [], after: [] });

                pseudoSelectors.before = pseudoSelectors.before.concat(rulePseudoSelectors.before);
                pseudoSelectors.after = pseudoSelectors.after.concat(rulePseudoSelectors.after);

            // Quietly handle errors from accessing cross-origin stylesheets.
            } catch (e) { if (console && console.warn) console.warn(e); }

            return pseudoSelectors;

        }, found);
}

我们可以使用这个字典来获取匹配这些选择器的元素上定义的伪元素数组:
function getPseudoElements() {
    var selectors = getPseudoElementSelectors(),
        names = ['before', 'after']

    return names.reduce(function(pseudoElements, name) {
        if (!selectors[name].length) return pseudoElements;

        var selector = selectors[name].join(','),
            elements = Array.from(document.querySelectorAll(selector));

        return pseudoElements.concat(
            elements.reduce(function(withContent, el) {
                var pseudo = getComputedStyle(el, name);

                // Add to array if element has content defined.
                return (pseudo.content.length)
                    ? withContent.concat(pseudo)
                    : withContent;
            }, [])
        );
    }, []);
}

最后,我使用的一个小型实用程序函数,用于将大多数DOM方法返回的类数组对象转换为实际数组:

Array.from = Array.from || function(arrayish) {
    return [].slice.call(arrayish);
};
Et voilà! 调用getPseudoElements()会返回一个CSS样式声明数组,对应于在文档中定义的伪元素,而不需要循环遍历和检查每个元素。 jsFiddle演示

注意事项

希望这种方法可以考虑到所有情况是不现实的。有一些需要注意的地方:

  • 它仅返回beforeafter伪元素,但很容易将其调整为包括其他伪元素,甚至是可配置列表。
  • 没有适当的CORS标头的跨域样式表将引发(被抑制的)安全异常,并且不会被包含。
  • 只有在CSS中设置的伪元素才会被捕获;直接在JavaScript中设置的伪元素将不会被捕获。
  • 一些奇怪的选择器(例如li[data-separator=","]:after之类的东西)将被破坏,但我相信我可以通过一些工作来使脚本免受这些问题的影响。

性能

性能将根据样式表中的规则数量和与定义伪元素的选择器匹配的元素数量而变化。如果您有大型样式表、相对较小的文档或更高比例的带有伪元素的元素,Max K的解决方案可能更快。

我在几个网站上进行了一些测试,以便了解不同情况下性能差异的情况。下面是在控制台中循环运行每个函数1000次的结果(Chrome 31):

  • Google (英国)
    • getPseudoElementsByCssSelectors: 757毫秒
    • getPseudoElements: 1071毫秒
  • Yahoo! UK
    • getPseudoElementsByCssSelectors: 59毫秒
    • getPseudoElements: 5492毫秒
  • MSN UK
    • getPseudoElementsByCssSelectors: 341毫秒
    • getPseudoElements: 12752毫秒
  • Stack Overflow
    • getPseudoElementsByCssSelectors: 22毫秒
    • getPseudoElements: 10908毫秒
  • Gmail
    • getPseudoElementsByCssSelectors: 42910毫秒
    • getPseudoElements: 11684毫秒
  • Nicholas Gallagher的纯CSS GUI图标演示
    • getPseudoElementsByCssSelectors: 2761毫秒
    • getPseudoElements: 948毫秒

代码用于测试性能

请注意,Max K的解决方案在最后两个示例中表现优异。我本来预计在Nicholas Gallagher的CSS图标页面上会这样,但没有想到在Gmail上也会这样!原来Gmail有将近110个选择器,指定了5个样式表中的伪元素,总共超过9600个选择器,这使得实际使用的元素数量相形见绌(大约2800个)。

值得注意的是,即使在最慢的情况下,Max的解决方案运行一次仍然不需要超过10ms,考虑到它只有我的四分之一长度,并且没有任何警告,这并不算太糟糕。


1
@DavidMulder,很高兴你喜欢这个答案,但我承认我对是否推荐它超过Max的答案也感到矛盾。我更详细地研究了性能(请参见更新),甚至有特定的情况下他的答案表现更好。鉴于其简单性和可靠性 - 特别是在从CDN加载CSS的情况下,正如你所提到的 - 在性能不关键的任何情况下,我都会倾向于使用他的答案。 - Jordan Gray

2

Max K分享了一种解决方案,即检查所有元素的计算样式,这是我自己过去一天中一直在使用的临时解决方案。巨大的缺点是性能开销,因为所有元素都会检查不存在的伪元素的计算样式两次(如果有伪元素可用,则我的脚本需要花费两倍的时间来执行)。

无论如何,只是想分享我过去几天一直在使用的稍微更普遍的版本。

var loopOverAllStyles = function(container,cb){
    var hasPseudo = function(el){
        var cs;
        return {
            after: (cs = getComputedStyle(el,"after"))["content"].length ? csa : false,
            before: (cs = getComputedStyle(el,"before"))["content"].length ? csb : false
        };
    }
    var allElements = container.querySelectorAll("*");
    for(var i=0;i<allElements.length;i++){
        cb(allElements[i],"element",getComputedStyle(allElements[i]));
        var pcs = hasPseudo(allElements[i]);
        if(pcs.after) cb(allElements[i],"after",pcs.after);
        if(pcs.before) cb(allElements[i],"before",pcs.before);
    }
}

loopOverAllStyles(document,function(el,type,computedStyle){
    console.log(arguments);
});

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