WebKit Inside: CSS 样式表的匹配时机介绍了当 HTML 页面有不同 CSS 样式表引入时,CSS 样式表开始匹配的时机。后续文章继续介绍 CSS 样式表的匹配过程,但是在匹配之前,首先需要收集页面里面的 Active 样式表。
1 Active 样式表
在一个 HTML 文件里面,可能会使用<style>
标签与<link>
标签引入许多样式表,但是这些样式表并不一定都同时在文档里面生效。有时根据业务需求,可能会只使用页面里的部分样式表。比如有一个换肤需求,页面里面可能会使用<link>
标签引入 4 张样式表,代码如下:
<link href="reset.css" rel="stylesheet" />
<link href="default.css" rel="stylesheet" title="Default Style" />
<link href="fancy.css" rel="alternate stylesheet" title="Fancy" />
<link href="basic.css" rel="alternate stylesheet" title="Basic" />
上面样式表reset.css
所在的<link>
标签有rel="stylesheet"
属性,没有title
属性,这种样式表被称为 Persisten 样式表,会一直被启用。
样式表default.css
所在的<link>
标签有rel="stylesheet"
和title
属性,这种样式表被称为 Preferred 样式表。Preferred 样式表是默认启用。一个页面只能有一个 Preferred 样式表。
样式表fancy.css
和basic.css
所在<link>
标签有rel="alternate stylesheet"
和title
属性,这种样式表被称为 Alternate 样式表。这些样式表默认下是不启用的,但是可以提供给用户选择。一旦用户选择了一个 Alternate 样式表,Preferred 样式表就会别禁用。
根据 <link>
标签语法,样式表reset.css
和default.css
会在页面里面使用,而样式表fancy.css
和basic.css
会暂时不使用。
一般这种场景会给一个按钮让用户切换皮肤,当用户选择切换到Fancy
皮肤时,样式表default.css
就失效,样式表fancy.css
就会启用。但是不管用户如何切换,样式表reset.css
始终有效。更多信息可以参考 MDN Alternative Style Sheet[1]。
在用户没有换肤之前,样式表reset.css
和default.css
样式表就属于 Active 样式表,当用户选择切换之后,样式表reset.css
和fancy.css
就是 Active 样式表。
在进行 CSS 样式表匹配之前,WebKit 首先要收集页面里面所有的 Active 样式表,然后依次遍历这些 Active 样式表的 CSS Rule 进行匹配。
2 相关类图
上面类图里Style::SCope
类持有负责进行样式表匹配的Style::Resolver
类,同时它内部还有 3 个重要的数据成员:m_styleSheetCandidateNodes
是一个哈希链表,用来按顺序存储 HTML 文件里面的 <style>
与<link>
节点,也就是 HTMLStyleElment
对象和HTMLLinkElement
对象。
m_activeStyleSheets
是一个 Vector,类似数组,用来顺序存储页面里面的 Active 样式表。
m_styleSheetsForStyleSheetList
也是一个 Vector,用来顺序存储页面里面的所有样式表。
3 获取 Candidate Node
无论内部样式表,还是外部样式表,当 WebKit 解析到 <style>
标签或者<link>
标签时,都会调用Style::Scope::addStyleSheetCandidateNode
方法,将自己添加到Style::Scope
的实例变量m_styleSheetCandidateNode
里面。
以内部样式表为例,下面是调用堆栈:
函数Style::Scope::addStyleSheetCandidateNode
的代码如下:
void Scope::addStyleSheetCandidateNode(Node& node, bool createdByParser)
{
if (!node.isConnected())
return;
// Until the <body> exists, we have no choice but to compare document positions,
// since styles outside of the body and head continue to be shunted into the head
// (and thus can shift to end up before dynamically added DOM content that is also
// outside the body).
// 1. createByParser 代表当前的 node 是从 HTML 文件里解析出来的,而不是通过 JavaScript 代码动态创建的;
// m_document.bodyOrFrameset 方法判断当前页面是否解析出了 <body> 标签和 <frameset> 标签;
// 如果前这两个条件为真,那么节点直接添加到变量 m_styleSheetCandidateNodes;
// 还有一种情形 m_styleSheetCandidateNodes 当前还没有添加任何节点
if ((createdByParser && m_document.bodyOrFrameset()) || m_styleSheetCandidateNodes.isEmptyIgnoringNullReferences()) {
m_styleSheetCandidateNodes.add(node);
return;
}
// Determine an appropriate insertion point.
// 2. 如果上述条件不满足,就会走到这里,这里会将当前节点与 m_styleSheetCandidateNodes 里已有的 <style> 或者 <link>
// 节点进行位置比较,以便按照正确的顺序把当前节点插入到 m_styleSheetCandidateNodes.
auto begin = m_styleSheetCandidateNodes.begin();
auto end = m_styleSheetCandidateNodes.end();
auto it = end;
RefPtr<Node> followingNode;
do {
// 3. // 从后向前遍历
--it;
Ref<Node> n = *it;
unsigned short position = n->compareDocumentPosition(node);
// 4. DOCUMENT_POSITION_FOLLOWING 表示当前节点 node 位于节点 n 后面
if (position == Node::DOCUMENT_POSITION_FOLLOWING) {
if (followingNode)
// 5. 注意 followwingNode 位于节点 n 的后面,这里将节点 node 插入到 followwingNode 前面,
// 也就是刚好插儒道节点 n 的后面
m_styleSheetCandidateNodes.insertBefore(*followingNode, node);
else
// 6. 如果节点 node 位于节点 n 后面,但是节点插入之前节点 n 后面已经没有其他节点了,那么就直接
// 将节点 node 添加到节点 n 后面
m_styleSheetCandidateNodes.appendOrMoveToLast(node);
return;
}
followingNode = WTFMove(n);
} while (it != begin);
LOG_WITH_STREAM(StyleSheets, stream << "Scope " << this << " addStyleSheetCandidateNode() " << node);
// 7. 如果遍历到 m_styleSheetCandidateNodes 最前面,上面代码也没有找到合适的位置,
// 那么就将节点 node 插入到最前面.
m_styleSheetCandidateNodes.insertBefore(*followingNode, node);
}
上面代码注释 1 是向变量m_styleSheetCandidateNodes
添加 node 节点的第一处代码。变量createByParser
代表当前节点 node 是从 HTML 文件里解析出来的,而不是通过 JavaScript 代码动态创建出来的。函数document.bodyOrFrameset
代表当前是否已经解析出了<body>
标签后者<frameset>
标签。如果满足前面这两个条件,或者当前m_styleSheetCandidateNodes
里为空,那么就将当前 node 添加进去。
如果上面条件都不满足,代码会运行到注释 2 处。注释 2 后面的代码会将当前 node 节点与m_styleSheetCandidateNodes
变量里已有的<style>
后者<link>
标签的位置相比较,以便按照正确的位置将当前节点 node 插入到m_styleSheetCandidateNodes
。
那什么是正确的位置呢?
因为样式表的位置影响着样式表里 CSS Rule 中声明的优先级。比如 HTML 页面通过<link>
标签引入了 2 个样式表 A 与 B,其中样式表 B 位于 样式表 A 后面。如果样式表 A 有如下 CSS Rule:
div {
background-color: red;
}
样式表 B 的 CSS Rule 和样式表一样,只是设置背景色为蓝色:
div {
background-color: blue;
}
由于样式表 A 和 B 都是 Author 样式表[2],而且 Specificity[3] 也一样,因此声明的优先级取决于它们所在的位置。由于样式表 B 比样式表 A 更靠后,因此最终会应用样式表 B 中的背景色。
上面代码注释 3 处就是从后向前遍历m_styleSheetCandidateNodes
,以便找到这个正确位置。
注释 4 处比较节点n
与节点node
的位置关系[4]。如果节点node
位于节点n
的后面,也就是DOCUMENT_POSITION_FOLLOWING
,那么就可以插入节点。
DOCUMENT_POSITION_FOLLOWING
的意义是按照 DOM 树的 Tree Order[5][6] 进行遍历,节点node
位于位于节点n
之后。Tree Order 就是按照先序-深度优先(preorder,depth-first)遍历。
假设有如下的 HTML:
<html>
<head>
<link rel="stylesheet" href="./test1.css" />
<link rel="stylesheet" href="./test2.css" />
</head>
<body>
<div>Hello</div>
<p>World</p>
</body>
</html>
其 DOM 树结构如下:
按照 Tree Order 先序-深度优先遍历,那么就是先遍历根节点,然后遍历从左起第一棵子树,然后是第二棵子树,然后是第三棵子树...。
遍历的顺序如上图所示,遍历结果如下:html->head->title->link->link->body->div->p
。从遍历结果可以看到,第 2 个 <link>
标签位于第 1 个<link>
标签后面,也就是第 2 个 <link>
标签following
第 1 个 <link>
标签。
从遍历结果上看,按照先序-深度优先遍历的位置关系,正好是 HTML 文件里面各标签的书写位置关系。
代码注释 5、6、7 都是将节点node
插入到m_styleSheetCandidateNodes
合适的位置,也就是说 HTML 里面是按照什么顺序引入的样式表,m_styleSheetCandidateNodes
就是按照同样的顺序存储的<style>
或者<link>
标签节点。
4 获取 Active 样式表
无论内部样式表还是外部样式表,当其解析完成之后,都会调用对应的checkLoaded
方法。
内部样式表的调用如下:
void InlineStyleSheetOwner::createSheet(Element& element, const String& text)
{
...
auto contents = StyleSheetContents::create(String(), parserContextForElement(element));
m_sheet = CSSStyleSheet::createInline(contents.get(), element, m_startTextPosition);
...
// 1. 解析内部样式表
contents->parseString(text);
...
// 2. 调用 checkLoaded 方法
contents->checkLoaded();
...
}
上面代码注释 1 解析内部样式表。
代码注释 2 调用checkLoaded
方法。
外部样式表的调用如下:
void HTMLLinkElement::setCSSStyleSheet(const String& href, const URL& baseURL, const String& charset, const CachedCSSStyleSheet* cachedStyleSheet)
{
...
auto styleSheet = StyleSheetContents::create(href, parserContext);
initializeStyleSheet(styleSheet.copyRef(), *cachedStyleSheet, MediaQueryParserContext(document()));
// FIXME: Set the visibility option based on m_sheet being clean or not.
// Best approach might be to set it on the style sheet content itself or its context parser otherwise.
// 1. 解析外部样式表
if (!styleSheet.get().parseAuthorStyleSheet(cachedStyleSheet, &document().securityOrigin())) {
...
}
...
// 2. 调用 checkLoaded 方法
styleSheet.get().checkLoaded();
...
}
上面代码注释 1 解析外部样式表。
注释 2 解析调用checkLoaded
方法。
这两种情形的 checkLoaded
方法最终会调用到Scope::didChangeActiveStyleSheetCandidates
方法,代码如下:
void Scope::didChangeActiveStyleSheetCandidates()
{
scheduleUpdate(UpdateType::ActiveSet);
}
Scope::didChangeActiveStyleSheetCandidates
方法内部只调用了一个方法Scope::scheduleUpdate
,传给它的参数是UpdateType::ActiveSet
。
UpdateType
类型是一个枚举,定义在StyleScope.h
,其定义如下:
// 定义在 StyleScope.h
enum class UpdateType : uint8_t {
ActiveSet, // 代表一个样式表解析完成,称为了 Active 样式表
ContentsOrInterpretation
};
枚举UpdateType::ActiveSt
代表一个 Active 样式表可用了。
方法 Scope::scheduleUpdate
方法如下:
void Scope::scheduleUpdate(UpdateType update)
{
...
if (!m_pendingUpdate || *m_pendingUpdate < update) {
// 1. 这里设置 m_pendingUpdate
m_pendingUpdate = update;
...
}
...
// 2. 启动 Timer,Timer 的回调函数触发 Active 样式表的收集.
// 参数 0 代表不延时,立即触发
m_pendingUpdateTimer.startOneShot(0_s);
}
上面代码注释 1 设置Style::Scope
对象的一个变量m_pendingUpdate
,这个变量在后续触发 Active 样式表收集使用。
代码注释 2 启用一个 Timer,Timer 的回调函数触发 Active 样式表的收集流程。
Timer 的回调函数如下:
void Scope::pendingUpdateTimerFired()
{
/// 1. 触发 Active 样式表收集
flushPendingUpdate();
}
上面代码注释 1 调用Style::Scope::flushPendingUpdate
方法触发 Active 样式表收集。
方法Style::Scope::flushPendingUpdate
代码如下:
inline void Scope::flushPendingUpdate()
{
...
// 1. m_pendingUpdate 已经在方法 Style::Scope::scheduleUpdate 里设置
if (m_pendingUpdate)
flushPendingSelfUpdate();
}
上面代码注释 1 处变量m_pendingUpdate
已经在方法Style::Scope::scheduleUpdate
里面设置成了UpdateType::ActiveSset
,所以这里直接调用方法Style::Scope::flushPendingSelfUpdate
方法。
Style::Scope::flushPendingSelfUpdate
方法代码如下:
void Scope::flushPendingSelfUpdate()
{
ASSERT(m_pendingUpdate);
auto updateType = *m_pendingUpdate;
// 1. 清除 m_pendingUpdate 变量,给其置空
clearPendingUpdate();
// 2. 收集 Active 样式表
updateActiveStyleSheets(updateType);
}
上面代码注释 1 首先清除变量m_pendingUpdate
,给其置空。
代码注释 2 调用Style::Scope::updateActiveStyleShhets
方法开始真正的收集 Active 样式表。
方法Style::Scope::updateActiveStyleSheets
代码如下:
void Scope::updateActiveStyleSheets(UpdateType updateType)
{
...
// 1. 收集 Active 样式表.
// collection 变量里面存储着收集到的 Active 样式表和页面里面所有样式表.
auto collection = collectActiveStyleSheets();
// 2. 变量 activeCSSStyleSheets 里面存储收集到的 Active 样式表,collection 变量里的 Active 样式表会赋值给这个变量
Vector<RefPtr<CSSStyleSheet>> activeCSSStyleSheets;
// 3. 上面已经收集了最新添加的 Active 样式表,这里进行过滤,剔除那些比如样式表长度为 0 的样式表,
// 过滤后的结果存储在 activeCSSStyleSheets 中.
filterEnabledNonemptyCSSStyleSheets(activeCSSStyleSheets, collection.activeStyleSheets);
...
// 4. 将 Active 样式表存储到 m_activeStyleSheets
m_activeStyleSheets.swap(activeCSSStyleSheets);
// 5. 将所有样式表存储到 m_styleSheetsForStyleSheetList
m_styleSheetsForStyleSheetList.swap(collection.styleSheetsForStyleSheetList);
...
}
上面代码注释 1 调用方法Style::Scope::collectActiveStyleSheet
收集页面里面的 Active 样式表和所有样式表,将结果存储在变量collection
中。
代码注释 2 声明的变量activeCSSStyleSheets
会存储变量collection
中的 Active 样式表。
代码注释 3 现将变量collection
中的 Active 样式表进行过滤,剔除那些比如样式表长度为 0 的样式表,过滤后的结果存储在activeCSSStyleSheets
变量中。
代码注释 4 将变量activeCSStyleSheet
的值交换给实例变量m_activeStyleSheets
,也就是m_activeStyleSheets
现在存储着页面里面的 Active 样式表。
同理,代码注释 5 将页面里面所有的样式表存储在实例变量m_styleSheetsForStyleSheetList
里。
下面看一下 Active 样式表的收集过程,也就是函数Style::Scope::collectActiveStyleSheet
,代码如下:
auto Scope::collectActiveStyleSheets() -> ActiveStyleSheetCollection
{
...
// 1. 存储 Active 样式表
Vector<RefPtr<StyleSheet>> sheets;
// 2. 存储 HTML 页面里面所有样式表
Vector<RefPtr<StyleSheet>> styleSheetsForStyleSheetsList;
// 3. 遍历之前存储在 m_styleSheetCandidateNodes 里的 <style> 或者 <link> 标签节点对象
for (auto& node : m_styleSheetCandidateNodes) {
RefPtr<StyleSheet> sheet;
if (is<ProcessingInstruction>(node)) {
// 4. ProcessingInstruction 就是诸如 <?xml> 这样的标签
...
} else if (is<HTMLLinkElement>(node) || is<HTMLStyleElement>(node) || is<SVGStyleElement>(node)) {
Element& element = downcast<Element>(node);
...
// Get the current preferred styleset. This is the
// set of sheets that will be enabled.
if (is<SVGStyleElement>(element))
sheet = downcast<SVGStyleElement>(element).sheet();
else if (is<HTMLLinkElement>(element))
// 5. 获取外部样式表
sheet = downcast<HTMLLinkElement>(element).sheet();
else
// 6. 获取内部样式表
sheet = downcast<HTMLStyleElement>(element).sheet();
if (sheet)
// 7. 将样式表添加到 styleSheetsForStyleSheetsList
styleSheetsForStyleSheetsList.append(sheet);
// Check to see if this sheet belongs to a styleset
// (thus making it PREFERRED or ALTERNATE rather than
// PERSISTENT).
auto& rel = element.attributeWithoutSynchronization(relAttr);
if (!enabledViaScript && sheet && !title.isEmpty()) {
...
// 8. 如果 <link> 标签的 rel 属性包含 alternate,并且有 title,这里将 sheet 设置为 null,
// 后面也添加不到 Active 样式表了.
if (title != m_preferredStylesheetSetName)
sheet = nullptr;
}
// 9. 如果 <link> 标签的 rel 属性包含了 alternate,并且没有 title 属性,那么也将 sheet 设置为 null,
// 后面也添加不到 Active 样式表了.
if (rel.contains("alternate"_s) && title.isEmpty())
sheet = nullptr;
...
}
if (sheet)
// 10. 将当前样式表添加到 Active 样式表中
sheets.append(WTFMove(sheet));
}
...
// 11. 将结果返回
// sheets 存储 Active 样式表
// styleSheetsForStyleSheetsList 存储所有样式表
return { WTFMove(sheets), WTFMove(styleSheetsForStyleSheetsList) };
}
上面代码注释 1 声明变量sheets
用来存储页面里面的 Active 样式表。
代码注释 2 声明变量styleSheetsForStyleSheetsList
用来存储页面里面的所有样式表。
代码注释 3 遍历之前存储在m_styleSheetCandidateNodes
实例变量里面的<style>
标签和<link>
标签节点对象。
代码注释 4 处理Processing Instruct
[7],不在收集 Active 样式表考虑之内。
代码注释 5 和代码注释 6 根据遍历的节点对象,从其上面获取到对应的样式表对象sheet
。
代码注释 7 将上面获取到的注释表对象存储到变量styleSheetsForStyleSheetsList
,这样styleSheetsForStyleSheetsList
里面就是存储的是页面里面所有的样式表。
代码注释 8 处理 Alternate 样式表,也就是<link>
标签的rel
属性包含alternate
,并且title
属性有值,此时代码将变量sheet
设置为null
,这样后续这张样式表就添加不到 Active 样式表里面了。
代码注释 9 同样也是处理 Alternate 样式表,使其后续无法添加到 Active 样式表里面。
代码注释 10 将获取到的样式表添加到sheets
变量,也就是 Active 样式表中。
代码注释 11 将收集的 Active 样式表和页面里面所有样式表返回出去。
5 小结
要获取 HTML 样式表里的 Active 样式表,首先就要获取页面里面的<style>
标签节点对象和<link>
标签节点对象。这些节点对象在<style>
标签和<link>
标签插入到 DOM 树时按照 TreeOrder 顺序存储在Style::Scope
的实例变量m_styleSheetCandidateNodes
中。
然后,当内部样式表或者外部样式表解析成功之后,会触发 Active 样式表的收集,收集过程就是遍历Style::Scope
的实例变量m_styleSheetCandidateNodes
,将<style>
标签节点或者<link>
标签节点关联的样式表收集到Style::Scope
的实例变量m_activeStyleSheets
中。
MDN Alternative Style Sheet ↩︎
MDN Introducing the CSS Cascade ↩︎
MDN Specificity ↩︎
MDN compareDocumentPosition ↩︎
https://dom.spec.whatwg.org/#concept-tree-order ↩︎
https://dom.spec.whatwg.org/#dom-node-document_position_following ↩︎
MDN ProcessingInstruction ↩︎