CocoaUI 的 CSS 样式应用算法说明和源码解析
W3C 规范中对 CSS 样式的应用算法有规定, 这个规范中的算法比较复杂, 简单来说, 就是根据 CSS 样式选择器中的不同类型的元素出现的次数来计算优先级, 如果某个节点同时命中多个 CSS 样式规则, 以高优先级的样式为准.
W3C 规范具体可以见这个文档: http://www.w3.org/TR/CSS2/cascade.html , "6.4.3 Calculating a selector's specificity" 一节.
例如下面的两条 CSS 样式规则和 HTML:
<style> ul li .clz{color: #33f;} li .clz{color: #f33;} </style> <ul> <li><span class="clz">Hello World!</span></li> </ul>
如果按照 W3C 规范来计算优先级, 那么会计算出:
第一条的优先级: a=0, b=0, c=1, d=2 第二条的优先级: a=0, b=0, c=1, d=1
那么, "Hello World!" 文字的颜色应该是蓝色(在现代浏览器中确实是蓝色). 但是, CocoaUI 做了一些改变, 因为这种计算优先级的算法略复杂, 不符合人直观的看法, 所以 CocoaUI 采用的出现的先后顺序算法, 后定义的样式的优先级更高并覆盖前面的定义. 所以, 对于 CocoaUI, 字体的颜色是红色的.
当然, 先来后到并不是 CocoaUI 应用 CSS 样式的唯一优先级, 因为有时更精准的规则, 我们直观地认为它的优先级应该更高. 所以, CocoaUI 定义了下面的优先级(从低到高):
- 默认 CSS
- 标签 CSS
- 类选择器 CSS
- ID 选择器 CSS
- 内联 CSS(HTML 标签中通过 style 属性定义)
- 动态修改的样式(包括修改 class)
这个优先级并不是凭空臆造出来的, 和 CSS 规范是基本相符的, 具体可以参考 Wiki .
前面说的是算法和流程, 那么具体到代码中应该怎么实现呢? 首先, 需要关注 IStyleDeclBlock 类.
IStyleDeclBlock 类是一种列表(数组)结构, 所以, 只要根据前面定义的优先级把各种样式添加进去, 当你从前往后遍历这个数组时, 依次应用(渲染)样式即可. 在解析 HTML/XML 时(类 IViewLoader), 按优先级顺序将样式已经加到 IStyleDeclBlock 列表里了.
类: IViewLoader // 1. builtin(default) css // 2. tagName css // 3. class css // 4. ID css // 5. inline css // $: dynamic set css if(defaultCss){ [view.style set:defaultCss]; } [view.style setTagName:tagName]; if(attributeDict){ NSString *class_ = [attributeDict objectForKey:@"class"]; if(class_ != nil){ NSMutableArray *ps = [NSMutableArray arrayWithArray: [class_ componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; [ps removeObject:@""]; for(NSString *clz in ps){ [view.style addClass:clz]; } } NSString *id_ = [attributeDict objectForKey:@"id"]; if(id_ != nil && id_.length > 0){ [_viewsById setObject:view forKey:id_]; [view.style setId:id_]; } NSString *css = [attributeDict objectForKey:@"style"]; if(css){ [view.style set:css]; } }
具体的样式渲染代码在 IStyle 类里:
类: IStyle - (void)renderAllCss{ [self reset]; for(IStyleDecl *decl in _declBlock.decls){ if(decl.isId || decl.isClass || decl.isTagName){ IStyleSheet *sheet = _view.inheritedStyleSheet; NSString *v = decl.val; for(IStyleRule *rule in sheet.rules){ if([rule.selectors containsObject:v] && [rule match:_view]){ for(IStyleDecl *decl in rule.declBlock.decls){ [self applyDecl:decl baseUrl:rule.declBlock.baseUrl]; } } } }else{ [self applyDecl:decl baseUrl:_declBlock.baseUrl]; } } }
因为 _declBlock 中的元素已经排好序, 所以我们直接遍历即可. 注意这段代码:
[rule.selectors containsObject:v]
假设 _declBlock 中一个名为 "clz" 的类(也就是当前的 DOM 节点 _view 定义了这个 class), 那么我们要先找出规则中包含 "clz" 的样式规则, 然后才判断是否匹配(match). 如果规则中不包含 "clz", 那就不要判断, 因为 match 方法并不区分匹配的类型, 而这里, 我们希望它只匹配特定的一种类型即可.
关于 match 的实现, 可以看这篇博客文章: ideawu - CSS 样式规则的匹配算法实现 .
CocoaUI 项目官方网站: http://www.cocoaui.com/ , 源码下载: https://github.com/ideawu/cocoaui