通过 JavaScript 更新样式信息
className 与 style 属性

我刚读了一篇关于 quirksmode.org 的有趣文章。这篇文章讨论了使用 className 与使用 style 作为通过 JavaScript 更新样式信息的方法,并得出结论:除了 Safari,classNamestyle 更快。

尽管所讨论的测试非常出色,但作者得出的普遍结论实际上是不正确的。该测试通过 getElementsByTagName 获取表格中的所有单元格。然后遍历这些单元格,在一种情况下改变元素的类名,在另一种情况下只更新内联样式。

然而,getElementsByTagName 返回的列表有一个关键的细微之处,那就是它根本不是一个真正的列表。它更像是一个文档的实时查询。这种实时查询在性能方面实际上非常复杂,因为它必须始终反映正确的节点集,即使在 DOM 中发生动态更改时也是如此。

因此,这个看似简单的测试实际上比你想象的要复杂得多。在 Safari 中,我们尝试在您浏览这个实时查询时缓存信息,记住长度和您查看的最后一项,假设最常见的操作将是向前迭代这个“对象数组”。然而,如果 DOM 发生变化,缓存的长度和项目可能会失效,因为——再次强调——这不是一个简单的列表。它是所有匹配查询的节点的实时视图。

在设置 className 的情况下,Safari 会立即更新 class 属性。对于内联样式,Safari 更为复杂。尽管它会更新解析后的声明,但它不会立即更新相应的 style 属性。它只是将其标记为“脏”,并且只有在有人请求该值时才会更新。这意味着在重复更新类名的情况下,DOM 在 Safari 中会发生变化,但在重复内联样式更新的情况下则不会。

Safari 的真正缺陷在这里暴露无遗。保持迭代速度的查询缓存状态在属性更改时(而不仅仅是在添加和删除元素和节点时)被错误地失效了。这意味着您在每次迭代中都在重新计算列表的长度,并在每次迭代中艰难地寻找第 n 个项目。尽管设置类名的操作比内联样式快得多,但使用这种实时查询(以及其中的一个错误,而不是样式系统中的错误)导致了性能大幅下降。

我修复了我们的代码,使其只在添加和删除元素时才正确失效。我很高兴地报告,在我 1.33 GHz 的笔记本电脑上,内联样式设置的测试耗时 100 毫秒,而设置类名时耗时 49 毫秒。一如既往地感谢 ppk 编写了出色的测试用例,并揭示了我们 getElementsByTagName 实时查询实现中的一个巧妙错误,尽管他得出了错误的普遍结论。

我认为,如果有一个页面尝试使用所有可能的不同方式遍历 DOM,以查看哪种技术最快,并帮助浏览器确保所有可能的“遍历 DOM”方式都保持快速,那将是很有趣的。想到的例子包括 TreeWalkerNodeFiltergetElementsByTagName、用于图像和链接的 HTML 集合、childNodes,以及 firstChild/nextSibling