更高效、更安全地操作 CSSOM :CSS Typed OM

NimitzDEV發表於2018-10-17

前言

長期以來,我們要修改 DOM 元素的樣式,我們實際上操作的是 CSS 的物件模型 CSSOM。而 Houdini 中推進的又一組 CSS 物件模型 Typed OM,該標準又給我們帶來了什麼好處呢?

CSSOM

CSSOM 是幹嘛的?

簡單的說來,CSSOM 是一組能讓 JS 操作元素 CSS 的 API。在瀏覽器進行頁面渲染的過程中扮演著非常重要的角色,瀏覽器的渲染步驟大致包括:

  1. 解析 HTML 內容並構建成 DOM 物件
  2. 解析 CSS 內容並構建成 CSSOM 物件
  3. 瀏覽器將 DOM 與 CSSOM 組合成渲染樹
  4. 最終瀏覽器將結果進行渲染

面臨的問題

在平時開發中,我們通過元素上的 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 物件。

帶來的好處

  1. 更少的 bug,正如前面所展示的操作,通過 TypedOM 進行操作減少此型別的問題;
  2. 在數值物件上呼叫簡單的算術運算方法,絕對單位之間還能方便得盡興單位轉換;
  3. 更好的效能,由於減少了字串操作,對於 CSSOM 的操作效能得到了更進一步的提升,由 Tab Akins 提供的測試表明,操作 Typed OM 比直接操作 CSSOM 字串帶來了大約 30% 的速度提升;
  4. 錯誤處理,對於錯誤的 CSS 值,將會丟擲錯誤;
  5. 鍵名與常規 CSS 寫法保持一致,不用在 backgroundColor 和 background-color 的邊緣試探;
  6. 由於 attributeStyleMap 以及 computedStyleMap 物件是個 map,這樣意味著我們可以使用標準 map 中提供的所有方法。

瀏覽器支援情況

目前各大瀏覽器廠商的實現情況:

更高效、更安全地操作 CSSOM :CSS  Typed OM

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)));
複製程式碼

數學操作方法

CSSMathValueCSSUnitValue 他們均繼承自 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可以傳入以下幾種引數:

  1. CSSRotate: 旋轉
  2. CSSScale: 縮放
  3. CSSSkew:傾斜
  4. CSSSkewX:X 軸傾斜
  5. CSSSkewY: Y 軸傾斜
  6. CSSTranslate: 轉換
  7. 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…

致謝

感謝安佳老師對本文提出的修改建議

相關文章