前言
長期以來,我們要修改 DOM 元素的樣式,我們實際上操作的是 CSS 的物件模型 CSSOM。而 Houdini 中推進的又一組 CSS 物件模型 Typed OM,該標準又給我們帶來了什麼好處呢?
CSSOM
CSSOM 是幹嘛的?
簡單的說來,CSSOM 是一組能讓 JS 操作元素 CSS 的 API。在瀏覽器進行頁面渲染的過程中扮演著非常重要的角色,瀏覽器的渲染步驟大致包括:
- 解析 HTML 內容並構建成 DOM 物件
- 解析 CSS 內容並構建成 CSSOM 物件
- 瀏覽器將 DOM 與 CSSOM 組合成渲染樹
- 最終瀏覽器將結果進行渲染
面臨的問題
在平時開發中,我們通過元素上的 style
物件去獲取元素的樣式:
const cover = document.getElementById('cover');
cover.style.opacity; // 假設是 0.5
複製程式碼
然後我們基於這個減少一點透明度:
cover.style.opacity += 0.3;
複製程式碼
那麼,這樣做有沒有問題呢?首先我們來看看值的型別:
typeof cover.style.opacity; // string
複製程式碼
What a suprise!,所以實際上,上面減少透明度的操作實際上產生了 0.50.3
的值,很顯然這是個有問題的操作。對於 height
等屬性,同樣的,返回了類似 200px
的字串,getComputedStyles
返回的數值也不例外。如果你想要將這些獲取出來的數值套入一些列的數學計算中,你必須先將其轉換成數字物件。為了解決這些問題,Typed OM 出現了。
Typed OM
Typed OM 的出現,給我們讀取以及設定數值新增了一種新的方法,不同於 CSSOM 中原有的字串值的表現形式,Typed OM 將 CSSOM 的數值以 map
的形式展現在元素的 attributeStyleMap
中,規則所對應的值則是更有使用價值的 JavaScript 物件。
帶來的好處
- 更少的 bug,正如前面所展示的操作,通過 TypedOM 進行操作減少此型別的問題;
- 在數值物件上呼叫簡單的算術運算方法,絕對單位之間還能方便得盡興單位轉換;
- 更好的效能,由於減少了字串操作,對於 CSSOM 的操作效能得到了更進一步的提升,由 Tab Akins 提供的測試表明,操作 Typed OM 比直接操作 CSSOM 字串帶來了大約 30% 的速度提升;
- 錯誤處理,對於錯誤的 CSS 值,將會丟擲錯誤;
- 鍵名與常規 CSS 寫法保持一致,不用在 backgroundColor 和 background-color 的邊緣試探;
- 由於
attributeStyleMap
以及computedStyleMap
物件是個map
,這樣意味著我們可以使用標準map
中提供的所有方法。
瀏覽器支援情況
目前各大瀏覽器廠商的實現情況:
Intent to implement: 有意向實現
Shipped: 已釋出
No signal: 暫無意圖
其中 Google Chrome 和 Opera 瀏覽器分別在 66 和 53 版中實現了。
可用性檢測方法
可以通過以下方法檢測是否可用:
window.CSS && CSS.number
複製程式碼
使用
基本的讀取和賦值方法
在 Typed OM 中,數值和數值的單位是分開的,所獲取的是一個 CSSUnitValue
物件,內建數值 value
和單位 unit
兩個鍵。
// 要對一個元素的樣式賦值,除了可以使用 CSS.px 構建之外,還能接受字串
el.attributeStyleMap.set('height', CSS.px(10));
el.attributeStyleMap.set('height', '10px');
// 對於獲取,返回 CSSUnitValue 物件,訪問其 value 屬性即可得到數字型別的值
el.attributeStyleMap.get('height').value; // 10
el.attributeStyleMap.get('height').unit; // 'px'
複製程式碼
CSS 數值型別
在 Typed OM 中,我們有兩種基本的數值型別,一種是上面例子中提到的數字加單位的簡單數值,他們屬於 CSSUnitValue
型別。對於不止於單個數字加單位或使用calc
計算的表示式,均屬於 CSSMathValue
型別。
CSSUnitValue
如上所述,CSSUnitValue
表達了簡單的數字加單位的 CSS 數值,同時你也可以通過對其使用 new
來構造一個,大多數情況下,你還能從 CSS 物件下的同名方法直接構造:
const num = CSS.number('10');
// num.value -> 10 num.unit -> 'number'
const px = CSS.px(42);
// px.value -> 42 px.unit -> 'px'
// 同樣可以使用 new 方法構造一個
const deg = new CSSUnitValue(45, 'deg');
// deg.value -> 45 deg.unit -> 'deg'
複製程式碼
完整的方法列表,可以檢視 CSS Typed OM 草案的內容。
CSSMathValue
如果你要表達涉及不止一個數值以及使用 calc
計算表示式的數值,則需要使用 CSSMathValue
。需要注意的是, calc
在實際使用中被瀏覽器求值之後,獲取到的是運算結果,也就是一個 CSSUnitValue 值。
既然涉及到表示式,自然少不了操作符,CSSMathValue
中還提供了基本的數學操作符:
// 求和操作: calc(100vw + -10px)
new CSSMathSum(CSS.vw(100), CSS.px(-10));
// 求積: calc(45deg * 3.1415926)
new CSSMathProduct(CSS.deg(45), CSS.number(Math.PI));
// 取相反數: calc(-10px)
new CSSMathNegate(CSS.px(10));
// 取倒數: calc(1 / 10px);
new CSSMathInvert(CSS.px(10));
// 範圍限制: calc(1px);
// 其中第一個引數為最小值,第三個引數為最大值,中間數值為需要鉗制的數值
new CSSMathClamp(1, -1, 3);
// 最大值: max(10%, 10px)
new CSSMathMax(CSS.percent(10), CSS.px(10));
// 最小值: min(10%, 10px)
new CSSMathMin(CSS.percent(10), CSS.px(10));
複製程式碼
表示式需要更復雜的怎麼辦?
以上的數學操作表示式符號均支援巢狀使用,例如需要構建表示式:calc(1px * (3px + 2em))
,可以做如下巢狀實現:
new CSSMathProduct(CSS.px(1), new CSSMathSum(CSS.px(3), CSS.em(2)));
複製程式碼
數學操作方法
CSSMathValue
和 CSSUnitValue
他們均繼承自 CSSNumericValue
,自然地也繼承了CSSNumericValue
上的數學操作方法,方便使用:
// 加: 1px + 1px
CSS.px(1).add(1);
// 減: 1px - 1px
CSS.px(1).sub(1);
// 乘: 1px * 3px
CSS.px(1).mul(3);
// 除: 1px 除 3px
CSS.px(1).div(3);
// 比較最大值: max(50%, 50vw);
CSS.percent(50).max(CSS.vw(50));
// 比較最小值: min(50vh, 50vw);
CSS.vh(50).min(CSS.vw(50));
// 相等比較方法,返回一個布林值 true
CSS.px(200).equals(CSS.px(200));
複製程式碼
同時,加減乘除這些操作同樣支援多個引數使用
// 累加 calc(10px + 10vw + 10%)
CSS.px(10).add(CSS.vw(10), CSS.percent(10));
複製程式碼
除此之外,絕對單位之間還能相互轉換:
CSS.in(9).to('cm').value;
// 22.860000000000003
複製程式碼
CSS Transform 數值型別
對於 CSS Transform 的 transform
屬性,上面的基本數值表達完全無法滿足,從而需要藉助 CSSTransformValue
,構建 CSSTransformValue
可以傳入以下幾種引數:
CSSRotate
: 旋轉CSSScale
: 縮放CSSSkew
:傾斜CSSSkewX
:X 軸傾斜CSSSkewY
: Y 軸傾斜CSSTranslate
: 轉換CSSPerspective
: 視角
與平常的 CSS 用法一樣,skew(x, y) 與分別 skewX(x) skewY(y) 產生的結果也是不一樣的,這點需要注意一下
用起來也是同樣的簡單:
// 轉變 transform: rotateX(45deg) scale(0.5) translate3d(10px, 10px, 10px);
new CSSTransformValue([
new CSSRotate(CSS.deg(45)),
new CSSScale(CSS.number(0.5), CSS.number(0.5)),
new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10))
]);
複製程式碼
對於CSSTranslate
型別,你還可以訪問物件上的 is2D
方法檢視當前 translate
是 2D 的還是 3D 的。同時,還能呼叫 toMatrix
方法獲得 DOMMatrix
矩陣物件。
CSS 位置數值型別
對於需要描述 x/y 位置的屬性,例如 object-position
,則需要用到 CSSPositionValue
型別。
const pos = new CSSPositionValue(CSS.px(5), CSS.px(10));
// pos.y.value -> 10 pos.x.value -> 10
複製程式碼
數值解析
既然我們可以在 Type OM 的物件上使用 toString()
方法得到字串規則,那麼我們是否能通過 API 將字串規則解析成 Type OM 的型別呢?答案是可以的。使用 CSSStyleValue
中的 parse
方法即可:
CSSStyleValue.parse('transform', 'translate(10px) scale(0.5)');
// 將會解析成 CSSTransformValue 物件
CSSStyleValue.parse('height', '2px');
// 將會解析成 CSSUnitValue 型別
複製程式碼
computedStyleMap
與傳統呼叫 window.getComputedStyle
方法相同,元素上的 computedStyleMap
方法同樣會返回所有的計算後屬性值。但它們仍然有一些小區別。window.getComputedStyle
仍然會返回字串數值;而對於 computedStyleMap
方法來說,返回的數值則是轉換成 Type OM 數值型別的。
document.body.attributeStyleMap.set('opacity', 1);
document.body.computedStyleMap().get('opacity').value;
// 1
window.getComputedStyle(document.body).opacity;
// '1'
複製程式碼
Typed OM 在 Houdini 其他標準中的角色
既然 Typed OM 涉及到了 CSSOM 的數值,那麼與之相關的標準中的數值都將與此相關。前段時間看了安佳老師的文章《CSS Paint API》的同學可能會對裡面的棋盤例子有印象,CSS Paint API 對 paint 引數的輸入值其實也是 CSS Typed OM 中的數值型別。
除此之外,Typed OM 的使用在為往後更高效地發展各個 Houdini 標準打下了基礎(包括自定義屬性,佈局以及繪製相關標準)。
總結
CSS Typed OM 解決了開發時修改數值的問題,同時通過減少字串操作增加了總體的操作效能,使得我們在操作 CSSOM 不僅方便還高效,配合 requestAnimationFrame
還能製作出效能更優的自定義動畫。
參考連結
drafts.css-houdini.org/css-typed-o…
developers.google.com/web/updates…
rocks1635.rssing.com/chan-409413…
致謝
感謝安佳老師對本文提出的修改建議