上一篇把八类绑定语法过了一遍,其中 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">
<div v-bind:class="{ active: isActive }"></div>
</div>
<script>
var app = new Vue({
el: "#app",
data: {
msg: "对象语法",
isActive: true
}
});
</script>
isActive 为 true 时渲染成 class="active",改成 false 类名就没了。注意键名如果带连字符,比如 text-danger,得写成 'text-danger': hasError,加引号,否则会被 JS 当成减法表达式。
# 2.2 和静态 class 共存
v-bind:class 不会覆盖标签上原有的 class,两者是合并关系:
<style>
.active {
width: 100px;
height: 100px;
background: red;
}
</style>
<div id="app">
<div class="box" v-bind:class="{ active: isActive }"></div>
</div>
<script>
var app = new Vue({
el: "#app",
data: {
msg: "对象语法",
isActive: true
}
});
</script>
最终渲染出来是 class="box active"。这个特性用得很多,静态类负责基础样式,动态类负责状态叠加,职责分得很清楚。原文这段的 CSS 直接裸写在 HTML 里没有包 <style> 标签,上面补上了。
# 2.3 直接绑定 data 里的一个对象
模板里的对象字面量一长就影响可读性,可以把整个对象搬到 data 里:
<style>
.active1 {
width: 100px;
height: 100px;
margin-top: 10px;
border: 1px solid #ccc;
}
</style>
<div id="app">
<div v-bind:class="classObj"></div>
</div>
<script>
var app = new Vue({
el: "#app",
data: {
classObj: {
active: true,
aaa: false
}
}
});
</script>
模板一下子干净了。代价是这个对象里的每个键都得提前在 data 里声明好,后加的键在 Vue 2 里不是响应式的,得走 this.$set。
# 2.4 用计算属性返回对象
这是官方文档专门点名的一种模式,也是我实际项目里用得最多的:
<style>
.aaa {
background: green;
width: 100px;
height: 100px;
margin-top: 10px;
}
</style>
<div id="app">
<div v-bind:class="classObject"></div>
</div>
<script>
var app = new Vue({
el: "#app",
data: {
msg: "对象语法",
isActive: true
},
computed: {
classObject: function () {
return {
aaa: this.isActive
}
}
}
});
</script>
强在哪?强在它把「什么条件下加什么类」这段逻辑从模板里搬到了 JS 里。真实业务的判断条件从来不是一个布尔,往往是「状态是 pending 且当前用户有权限且不在只读模式」。这种条件塞进模板就是灾难,放进计算属性里可以写 if else、可以写注释、可以单独测。
而且计算属性有缓存,依赖不变就不会重新计算,比在模板里写复杂表达式效率高。计算属性和侦听器的细节在 vue计算属性与数据监听(十) 里。
原文这里把计算属性命名成了 Obj,首字母大写在 JS 里通常表示构造函数,容易误导,上面改成了 classObject,功能一样。
# 三、绑定 class 的数组语法
数组语法传的是一组类名,Vue 把它们拼起来。
# 3.1 基本形态
<style>
.active {
width: 100px;
height: 100px;
background: red;
}
.active1 {
color: yellow;
}
.aaa {
border: 5px solid #ccc;
}
</style>
<div id="app">
<div class="box" v-bind:class="[isActive, isActive1, isActive2]">{{ msg }}</div>
</div>
<script>
var app = new Vue({
el: "#app",
data: {
msg: "数组语法",
isActive: "active",
isActive1: "active1",
isActive2: "aaa"
}
});
</script>
这里有个容易懵的点,数组里写的是变量名,变量的值才是类名字符串。isActive 这个名字起得有误导性,它看着像布尔,实际存的是 "active" 这个字符串。要直接写死类名,得加引号,写成 ['active', 'aaa']。
# 3.2 用三元表达式做条件
数组里的每一项都可以是表达式,所以条件类名可以这么写:
<div id="app">
<div class="box" v-bind:class="[isActive, isActive1, isActive5 ? isActive2 : '']">{{ msg }}</div>
</div>
<script>
var app = new Vue({
el: "#app",
data: {
msg: "数组语法",
isActive5: false,
isActive: "active",
isActive1: "active1",
isActive2: "aaa"
}
});
</script>
条件为假时返回空字符串,Vue 会自动过滤掉,不会渲染出多余的空格类名。
# 3.3 数组里嵌对象
三元只有一两个还行,条件一多整行就废了。这时候把对象语法塞进数组里:
<div id="app">
<div class="box" v-bind:class="[isActive, { active1: isActive5 }, isActive5 ? isActive2 : '']">{{ msg }}</div>
</div>
<script>
var app = new Vue({
el: "#app",
data: {
msg: "数组语法",
isActive5: true,
isActive: "active",
isActive1: "active1",
isActive2: "aaa"
}
});
</script>
数组负责「一定要加的类」,里面的对象负责「按条件加的类」,读起来比三元清楚。不过说实话,一旦写到这个复杂度,我更倾向于直接换成 2.4 那个计算属性方案。
# 四、在组件上使用 class
在自定义组件的标签上写 class,这些类会被加到组件的根元素上,根元素上已有的类不会被覆盖,两边是合并的:
<style>
.active1 {
width: 100px;
background: red;
}
.aaa {
border: 5px solid #ccc;
}
.bbb {
height: 100px;
}
</style>
<div id="app">
<tanchu v-bind:class="classObj"></tanchu>
</div>
<script>
Vue.component('tanchu', {
template: `<div class="bbb">
<input type="button" value="弹出"/>
</div>`
})
var app = new Vue({
el: "#app",
data: {
classObj: {
active1: true,
aaa: true
}
}
})
</script>
最终根元素上是 class="bbb active1 aaa"。这个行为让封装组件时很省心,使用方想给你的组件加个外边距,直接在标签上写类就行,不需要你在组件内部专门开一个 prop。
这里有个坑要注意。这套自动继承只在组件模板有单个根元素时才成立。如果模板有多个并列的根节点,Vue 不知道该往哪个上放,类名就会丢失。Vue 2 本来就不允许多根节点,所以碰不到;Vue 3 允许了,这个问题就浮出来了,后面第六节会说。