问题 如何遍历页面上包含伪元素的所有元素?


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


11798
2017-11-27 17:32


起源

我的理解是 所有 元素隐式地具有伪元素,并且它们的默认样式使它们不可见。 - zzzzBov
看看这个答案: stackoverflow.com/a/5041526/722135 - Babblo
好吧,你至少可以通过前面提到的API来选择样式,“证明”这个问题已经错了〜+那个问题是2年了,所以我暗中希望一些新的API可能已经发布了。 - David Mulder
好问题。我真的很讨厌伪元素的本质;他们为什么不能看到它们?如果我要编写规范,我会让它们实际上将内容插入到dom中。 - bjb568


答案:


你走在正确的轨道上。循环使用所有DOM元素相当容易 getElementsByTagName("*") 要么 querySelectorAll("*")。然后我们必须查看每个元素是否具有伪元素。所有这些都像@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);
    }
}

5
2017-11-28 22:59



哈哈,我的字面上已经存在相同的代码,等等,让我发布那个以及它的结构略有不同,这对某些人来说可能有用。我仍然希望看到一些较便宜的解决方案。 - David Mulder
顺便说一句,我会在寻找更好答案的同时在这个问题上给予赏金,但是否则你可以期待50个奖励代表:P - David Mulder
并非所有标签都可以在内容之前/之后输入,例如输入。你可以通过不击中所有标签来减少击中所有标签的开销,但只能减少“内容”标签列表。甚至更好的是像querySelectorAll(“div,a,i”)这样的短自定义白名单。你也可以从body / #main / form开始而不是root来跳过元,标题,链接等等。只是说... - dandavis
前几天我注意到了这个问题并且记得我从来没有感觉到获得赏金的时候,如果我不得不这样做并且性能可以接受,我可能只是使用 你的 回答。我们来解决这个问题。 :) - Jordan Gray


经过一些性能测试,我的建议是:

  •  情况,使用 Max K's 解。在大多数情况下,性能足够好,它是可靠的,并且它的时钟频率低于15 LOC(我的约为70)。
  • 使用下面的解决方案,如果你真的需要挤出每一毫秒,你知道(因为你已经测试过它)它更快。

(通常)更快的解决方案

您已经知道如何获取文档中每个元素的列表 document.querySelectorAll('*')。这在大多数情况下都适用,但对于只有少数元素具有伪元素的较大文档,它可能很慢。

在这种情况下,我们可以从不同的角度处理问题。首先,我们遍历文档样式表并构造与之关联的选择器字典 before 要么 after 伪元素:

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

Etvoilà! 调用 getPseudoElements() 返回一个CSS样式声明数组,对应于文档中定义的伪元素,而不循环遍历并检查每个元素。

jsFiddle演示

注意事项

希望这种方法可以解决所有问题,这太过分了。有几点需要注意:

  • 它只返回 before 和 after 伪元素,虽然很容易使其适应包括其他元素,甚至是可配置列表。
  • 没有相应CORS标头的跨域样式表将引发(抑制)安全异常,并且不会包含在内。
  • 只会在您的CSS中设置伪元素;直接在JavaScript中设置的那些不会。
  • 一些奇怪的选择器(例如,像 li[data-separator=","]:after虽然我很确定我可以通过一些小工作来防止脚本对抗大多数这些,但是会被破坏。

性能

性能将根据样式表中的规则数量以及与定义伪元素的选择器匹配的元素数量而有所不同。如果您有大型样式表,相对较小的文档或具有伪元素的较高比例的元素, Max K's 解 可能会更快。

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

  • 谷歌(英国)
    • getPseudoElementsByCssSelectors:757毫秒
    • getPseudoElements:1071ms
  • 雅虎联合王国
    • getPseudoElementsByCssSelectors:59ms
    • getPseudoElements:5492ms
  • MSN UK
    • getPseudoElementsByCssSelectors:341毫秒
    • getPseudoElements:12752ms
  • 堆栈溢出
    • getPseudoElementsByCssSelectors:22ms
    • getPseudoElements:10908ms
  • Gmail的
    • getPseudoElementsByCssSelectors:42910毫秒
    • getPseudoElements:11684ms
  • Nicholas Gallagher的纯CSS GUI图标演示
    • getPseudoElementsByCssSelectors:2761毫秒
    • getPseudoElements:948ms

用于测试性能的代码

请注意,Max K的解决方案在最后两个例子中击败了我的裤子。我期待它与Nicholas Gallagher的CSS图标页面,但不是Gmail!事实证明,Gmail总共有近110个选择器,它们在5个样式表中指定了伪元素,总共超过9,600个选择器,这使得实际使用的元素数量相形见绌(大约2,800个)。

值得注意的是,即使在最慢的情况下,Max的解决方案仍然不需要花费超过10毫秒的时间来运行一次,这也不错,因为它是我的长度的四分之一并且没有任何警告。


4
2017-11-29 13:27



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


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

2
2017-11-28 23:20