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