CSS 里的 8000 多种单位,终于被一个 number 类型全部装箱了——Typed OM 这块拼图 Chrome 八年前就放上了
你写 JS 的时候从来没想过一个问题:CSS 里的 8000 多种单位,到底能不能用一个 number 类型装下?前端人第一次认真想这件事,通常是被一个真实的 bug 逼的——比如 el.style.opacity 返回的是字符串 "0.30",你拿它和 0.3 做相等判断,"0.30" !== 0.3,一个字符串和一个数字,JavaScript 自己都分不清谁对谁错。Typed OM 里那个 CSSNumericValue,管的就是这件事。
先把结论放前面:浏览器这条线已经走了八年。Chrome 从 66 就上了 CSSStyleValue.parse(),Edge 79 跟上,Safari 要等到 16.4;Firefox 至今没完全落地,只挂在 Nightly 的预览支持里——所以你团队里那个坚持用 Firefox 开发的同事,不是你代码 bug 的唯一来源,是这台浏览器压根没接上这根线。而 CSSNumericValue 的身份很明确,它是 CSSUnitValue 和 CSSMathValue 的父接口,一个 number 类型管住了 CSS 里所有能参与计算的数值。
问题的根:CSS 值一直是字符串,JavaScript 一直在猜
以前的 CSSOM 是一个纯字符串的接口:getComputedStyle 返回字符串,el.style.width 返回字符串。工程上最顶不住的一点是,字符串这东西没有类型,也没有单位。
看一个真实项目里天天出现的写法:
const w = window.getComputedStyle(el).width; // "200px"
// 你想把它和另一个值比较、或者加个 10
const next = parseFloat(w) + 10; // 210
el.style.width = next + "px"; // 你得自己再拼回去
每一步都在手工做类型系统该做的事。单位被扔了、精度被浮点吃了、值超出合法范围也没人管。Typed OM 做的是把”值和单位”从字符串里剥出来,变成真正带类型和单位语义的对象:
const { value, unit } = CSS.px(42);
// value === 42, unit === "px"
CSS.number("10"); // { value: 10, unit: "number" }
CSS.percent("10"); // { value: 10, unit: "percent" }
CSS.deg(45); // { value: 45, unit: "deg" }
CSS.* 这一串工厂函数就是 number 那个类型在具体单位上的落地。你拿到的不再是一个需要 parseFloat 的字符串,而是一个你确定知道它是什么、单位是什么的对象。
方案:CSSNumericValue 收编了 CSS 里所有的”数”
CSSNumericValue 管两类东西,这也是它被当成”最后一块拼图”的原因:
CSSUnitValue— 单个单位的值,比如"42px"、"50%"CSSMathValue— 多个值、多个单位的表达式,比如"calc(56em + 10%)"
连 calc()、min()、max() 都被建模成了对象,而不是字符串:
new CSSMathSum(CSS.vw(100), CSS.px(-10)).toString();
// "calc(100vw + -10px)"
new CSSMathMin(CSS.percent(80), CSS.px(12)).toString();
// "min(80%, 12px)"
new CSSMathMax(CSS.percent(80), CSS.px(12)).toString();
// "max(80%, 12px)"
好处不只是好看。官方给出的性能口径里,Typed OM 在每秒操作数上比老的 CSSOM 加字符串方案快大约 30% 量级,原因是引擎不再需要对字符串做序列化和反序列化,JS 侧和 C++ 侧对 CSS 值的理解终于对齐了。对于跑在 requestAnimationFrame 里的高频动画,这个差别会直接体现在帧率上。
更实用的是运算和类型推导。add()、sub()、mul()、div() 都挂在 CSSNumericValue 上,单位相同就返回 CSSUnitValue,不同就自动升级成 CSSMathSum:
let mathSum = CSS.px(23).add(CSS.percent(4)).add(CSS.cm(3)).add(CSS.in(9));
console.log(mathSum.toString());
// "calc(23px + 4% + 3cm + 9in)"
type() 方法会告诉你这个值到底是什么类型——length、angle、time、frequency、resolution、flex、percent 之一。举个例子,calc(1px * 1em) 返回的类型是 { length: 2 },因为它是长度单位的二次方。这种”编译器级别的类型感知”,在字符串时代是不存在的。
还有两个容易被忽略但工程上很值钱的细节。第一,computedStyleMap() 返回的是计算值,而 getComputedStyle() 返回的是解析值——Typed OM 会保留 width: 50% 里的百分比,CSSOM 会直接把它解析成 200px。做响应式逻辑时,这个差别决定了你拿到的信息有没有丢失。第二,Typed OM 会自动夹取和取整:opacity 设成 3,计算后会被夹到 1;z-index: 15.4,计算后会被舍成 15。这些以前要靠业务代码自己防的边界问题,现在由值本身负责。
两个你一定会踩的坑
坑一:CSSStyleValue.parse() 在 Worker 里不可用。 这是官方明确写了的限制——parse() 这个方法不能在 Worker 或 Worklet 上下文里调用。但 CSSNumericValue.parse() 是可以的,CSSNumericValue 的 type() 方法官方也标注了”available in Web Workers“。所以如果你的架构把 CSS 值计算放到 Worker 里,别用 CSSStyleValue.parse(),用 CSSNumericValue.parse():
CSSNumericValue.parse("42.0px");
// CSSUnitValue { value: 42, unit: "px" }
坑二:Firefox。 CSSStyleValue.parse()、CSSNumericValue.parse()、add()、type() 在 Firefox 全是”Preview support / Nightly”,Firefox for Android 直接不支持。Typed OM 不是一个你能无条件依赖的 Baseline 能力,落地时必须做特性检测,官方推荐的姿势是查工厂函数是否存在:
if (window.CSS && CSS.number) {
// Supports CSS Typed OM.
}
顺带说一句,CSSStyleValue.parse(property, cssText) 这条老路径本身也在被收窄——规范侧的意图是让你直接走类型化的 CSSNumericValue.parse(cssText),前者还额外要求你传一个属性名、会走属性语法解析,在 Worker 场景里直接不可用。新代码建议直接站在 CSSNumericValue 这条线上。
判断标准:什么时候值得引入
不需要全量迁移,按这个标准切:
- 你在 JS 里对 CSS 值做过
parseFloat+ 字符串拼接 → 换成CSS.px()/CSS.number()这套工厂函数 - 你在
requestAnimationFrame里每帧读写样式 → 这套是性能收益最明显的地方,值对象可以直接改属性再set回去,不用碰 DOM 读值 - 你在 Worker 里处理 CSS 值 → 只能用
CSSNumericValue.parse()这条路 - 你的用户里有 Firefox → 必须做
window.CSS && CSS.number特性检测,别裸用
一句话收尾:CSS Typed OM 不是”新 API 合集”,它是把 CSS 值从字符串变成带类型的对象——CSSNumericValue 就是那个管住所有数值的 number。八年前 Chrome 开了这个头,Safari 追了五年半,Firefox 还在路上。你的代码要不要今天就上,取决于你有多怕 "0.30" !== 0.3 这类 bug。
评论区
登录后可评论。