上一篇把八类绑定语法过了一遍,其中 class 和 style 是最容易写歪的两个。原因也简单,它们支持的写法太多了,对象、数组、数组里套对象、计算属性返回对象,四种写法能解决同一个问题,新手根本不知道该挑哪个。我自己刚写 Vue 那会儿是全用三元字符串拼的,一个标签上拼出三层嵌套三元,过两周回头看谁都不认识。这篇把 class 和 style 的全部写法排一遍,重点说清每种写法适合什么场景,以及那个官方称为「常用且强大」的计算属性模式到底强在哪。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 为什么 Vue 要给 class 和 style 开小灶,而不是当普通属性处理
- class 对象语法的四种形态,从写死的键值到计算属性返回对象
- class 数组语法,以及数组里嵌对象这个组合写法
- 在自定义组件上写 class 会发生什么,类名落到哪个元素上
- 内联 style 的对象语法、对象数组语法、自动前缀和多重值兜底
- 这些写法在 Vue 3 里有哪些变化
一、为什么 class 和 style 要特殊对待
v-bind 处理普通属性时逻辑很直白,表达式算出什么值就往属性上放什么值。但 class 和 style 不行。
你想想看,一个元素上的类名往往来自好几处。有布局用的固定类,有状态相关的 active、disabled,有主题相关的 dark。如果 :class 只接受字符串,那你每次都得自己拼,还得处理空格、处理某个类要不要加的判断,最后拼出一坨没法读的表达式。
所以 Vue 在 v-bind 里给这两个属性做了增强,允许传对象和数组,框架负责把它们展开成最终的字符串,还会自动和标签上写死的静态 class 合并。这一层增强省掉的正是字符串拼接那部分脏活。
二、绑定 class 的对象语法
对象语法的规则一句话,键是类名,值是布尔,值为真类名就上。数据属性一变,class 列表跟着更新。
2.1 最简形态
<div id="app"> |
isActive 为 true 时渲染成 class="active",改成 false 类名就没了。注意键名如果带连字符,比如 text-danger,得写成 'text-danger': hasError,加引号,否则会被 JS 当成减法表达式。
2.2 和静态 class 共存
v-bind:class 不会覆盖标签上原有的 class,两者是合并关系:
<style> |
最终渲染出来是 class="box active"。这个特性用得很多,静态类负责基础样式,动态类负责状态叠加,职责分得很清楚。原文这段的 CSS 直接裸写在 HTML 里没有包 <style> 标签,上面补上了。
2.3 直接绑定 data 里的一个对象
模板里的对象字面量一长就影响可读性,可以把整个对象搬到 data 里:
<style> |
模板一下子干净了。代价是这个对象里的每个键都得提前在 data 里声明好,后加的键在 Vue 2 里不是响应式的,得走 this.$set。
2.4 用计算属性返回对象
这是官方文档专门点名的一种模式,也是我实际项目里用得最多的:
<style> |
强在哪?强在它把「什么条件下加什么类」这段逻辑从模板里搬到了 JS 里。真实业务的判断条件从来不是一个布尔,往往是「状态是 pending 且当前用户有权限且不在只读模式」。这种条件塞进模板就是灾难,放进计算属性里可以写 if else、可以写注释、可以单独测。
而且计算属性有缓存,依赖不变就不会重新计算,比在模板里写复杂表达式效率高。计算属性和侦听器的细节在 vue计算属性与数据监听(十) 里。
原文这里把计算属性命名成了 Obj,首字母大写在 JS 里通常表示构造函数,容易误导,上面改成了 classObject,功能一样。
三、绑定 class 的数组语法
数组语法传的是一组类名,Vue 把它们拼起来。
3.1 基本形态
<style> |
这里有个容易懵的点,数组里写的是变量名,变量的值才是类名字符串。isActive 这个名字起得有误导性,它看着像布尔,实际存的是 "active" 这个字符串。要直接写死类名,得加引号,写成 ['active', 'aaa']。
3.2 用三元表达式做条件
数组里的每一项都可以是表达式,所以条件类名可以这么写:
<div id="app"> |
条件为假时返回空字符串,Vue 会自动过滤掉,不会渲染出多余的空格类名。
3.3 数组里嵌对象
三元只有一两个还行,条件一多整行就废了。这时候把对象语法塞进数组里:
<div id="app"> |
数组负责「一定要加的类」,里面的对象负责「按条件加的类」,读起来比三元清楚。不过说实话,一旦写到这个复杂度,我更倾向于直接换成 2.4 那个计算属性方案。
四、在组件上使用 class
在自定义组件的标签上写 class,这些类会被加到组件的根元素上,根元素上已有的类不会被覆盖,两边是合并的:
<style> |
最终根元素上是 class="bbb active1 aaa"。这个行为让封装组件时很省心,使用方想给你的组件加个外边距,直接在标签上写类就行,不需要你在组件内部专门开一个 prop。
这里有个坑要注意。这套自动继承只在组件模板有单个根元素时才成立。如果模板有多个并列的根节点,Vue 不知道该往哪个上放,类名就会丢失。Vue 2 本来就不允许多根节点,所以碰不到;Vue 3 允许了,这个问题就浮出来了,后面第六节会说。
五、绑定内联样式
5.1 对象语法
<div id="app"> |
原文这段 data 里 c: "100px" 后面漏了一个逗号,直接跑会抛语法错误,上面补上了。同时把 classObj 这个名字改成了 styleObj,样式对象叫 class 开头的名字实在容易看串。
两条写法规则记牢就不会错。属性名用驼峰,marginTop 而不是 margin-top,非要写连字符形式就加引号包起来。值必须是带单位的完整字符串,写 width: 100 没有任何效果,因为 Vue 只是把它塞进元素的 style 里,不会替你补 px。
5.2 数组语法
样式的数组语法和 class 不一样,它接受的是一组样式对象,后面的会覆盖前面的同名属性:
<div id="app"> |
典型用法是「基础样式对象加覆盖样式对象」,基础的那份写死,覆盖的那份根据状态算出来。
5.3 自动添加前缀和多重值
用到需要浏览器前缀的 CSS 属性时,比如 transform,Vue 会在运行时侦测当前浏览器支持哪种写法,自动补上对应前缀。你只管写标准属性名。
还有一个不太常见但很实用的能力,属性值可以传数组,Vue 会渲染数组里浏览器支持的最后一个值。做旧浏览器兜底时用得上,写法上就是给同一个属性给出多个候选值,从最保守的排到最新的。
回到这一节开头的问题,为什么要这么多写法。因为内联样式在 Vue 里承担的是「只能在运行时算出来的样式」,比如根据滚动距离算出来的高度、根据数据算出来的进度条宽度。凡是能提前写死在 CSS 类里的,都该走 class 而不是 style,内联样式的优先级太高,后期想覆盖非常难受。
六、Vue 3 里的差异
class 和 style 的绑定语法在 Vue 3 里几乎原样保留,对象、数组、计算属性这几种写法都能直接用,这块是迁移成本最低的部分之一。
差异集中在组件上。Vue 3 支持多根节点组件,这时候 class 和 style 不再自动落到某个根元素上,你需要在模板里显式绑定,写成 :class="$attrs.class" 指定落点,否则会有警告并且类名丢失。另外 Vue 2 里 $attrs 不包含 class 和 style,Vue 3 把它们合并进去了,写透传组件时这点要留意。
还有一个是 <style scoped> 的行为。Vue 3 里子组件的根节点会同时带上父组件的作用域 id,深度选择器的写法也从 ::v-deep 统一成了 :deep()。具体以官方文档为准,这块我只在 Vue 3 加 Vite 的项目里验证过。
总结
四种写法怎么选,我的判断标准是这样的。
只有一两个状态类,直接在模板里写对象字面量,最省事。类名固定但数量多,用数组。条件判断超过两个,或者判断逻辑本身要写注释才说得清,一律搬进计算属性返回对象,这是维护成本最低的方案。
其余几条容易踩的:
- 对象语法的键带连字符必须加引号
- 数组语法里放的是变量,变量的值才是类名,写死类名要加引号
- 静态 class 和动态 class 是合并不是覆盖,组件上写 class 会落到根元素
- style 的属性名用驼峰,值必须带单位
- 能用 class 表达的样式就别用内联 style,优先级太高不好覆盖
原文这篇里被我修掉的是 style 对象 data 里缺的那个逗号,以及两处会误导的变量命名。
上一篇是 vue中的数据绑定(二),下一篇 vue 基本指令(四) 会把 Vue 的常用指令按用途分类过一遍。