理解 class 与 style 的特殊绑定

Q: Vue3 中为什么要对 class 和 style 做特殊的功能增强?普通的属性绑定不能用吗?

A: 当然可以用,但在实际开发中,class 和 style 这两个属性使用频率极高,如果仅仅通过字符串拼接来动态控制样式,代码很容易变得混乱且难以维护。为此,Vue 专门针对这两个属性做了功能增强——除了字符串之外,还支持对象数组两种绑定方式,极大地提升了开发体验。

类绑定:对象语法

对象语法的核心思想很简单:对象的是要挂上去的样式类名,是一个布尔值,true 表示挂上该类,false 表示不挂。

<template>
  <div
    class="demo"
    :class="{
      'active': isActive,
      'text-danger': hasError,
    }"
  >
    绑定样式类
  </div>
</template>

<script setup>
import { ref } from 'vue';
const isActive = ref(true);
const hasError = ref(false);
setTimeout(() => {
  hasError.value = true;
}, 3000);
</script>

上面例子中,active 一开始就挂在元素上,而 text-danger 会在 3 秒后被动态添加。这种写法比手动拼接字符串 'demo' + (isActive ? ' active' : '') + (hasError ? ' text-danger' : '') 要清晰得多。

当需要绑定的类比较多时,推荐将对象写到 <script setup> 中,让模板保持清爽:

<template>
  <div class="demo" :class="classObj">绑定样式类</div>
</template>

<script setup>
import { reactive } from 'vue';
const classObj = reactive({
  'active': true,
  'isFinite': true,
  'text-danger': false,
});

setTimeout(() => {
  classObj['text-danger'] = true;
}, 3000);
</script>

更进一步,如果样式的切换涉及到复杂的逻辑判断,还可以绑定一个计算属性返回的样式对象:

<template>
  <div class="demo" :class="classObj">绑定样式类</div>
</template>

<script setup>
import { ref, computed } from 'vue';
const isActive = ref(true);
const error = ref(null);

const classObj = computed(() => {
  return {
    'active': isActive.value && !error.value,
    'text-danger': error.value && error.value.type === 'fatal',
  };
});
setTimeout(() => {
  error.value = { type: 'fatal' };
}, 3000);
</script>

使用计算属性的好处在于:当依赖的响应式数据发生变化时,样式对象会自动重新计算,保证逻辑始终与数据同步。

类绑定:数组语法

数组语法中,数组的每一项通常是一个 ref 值,该 ref 对应的就是实际要挂上的类名:

<template>
  <div class="demo" :class="[isActive, error]">绑定样式类</div>
</template>

<script setup>
import { ref } from 'vue';
const isActive = ref('active');
const error = ref('text-danger');
setTimeout(() => {
  isActive.value = 'normal';
}, 3000);
</script>

数组语法同样支持三目运算表达式

<template>
  <div :class="[isActive ? activeClass : '', errorClass]">绑定样式类</div>
</template>

<script setup>
import { ref } from 'vue';
const isActive = ref(false);
const activeClass = ref('active');
const errorClass = ref('text-danger');
setTimeout(() => {
  isActive.value = true;
}, 3000);
</script>

如果三目运算依赖多个条件,表达式会变得冗长,这时可以在数组中嵌套对象来简化:

<div :class="[{ active: isActive }, errorClass]"></div>

这种数组嵌套对象的写法兼具了灵活性和可读性,是实际项目中非常常见的模式。

内联样式绑定

相比 class 绑定,style(内联样式)的绑定更需要功能增强,因为样式规则本身就涉及键值对结构。

对象方式:

<template>
  <div
    :style="{
      color: activeColor,
      fontSize: fontSize + 'px',
    }"
  >
    绑定内联样式
  </div>
</template>

<script setup>
import { ref } from 'vue';
const activeColor = ref('red');
const fontSize = ref(30);
setTimeout(() => {
  activeColor.value = 'blue';
}, 3000);
</script>

style 绑定对象的键是 CSS 属性名(支持驼峰命名),值对应 ref 形式的属性值。同样,也可以将样式对象提前声明在 script 中,保持模板整洁:

<template>
  <div :style="styleObj">绑定内联样式</div>
</template>

<script setup>
import { reactive } from 'vue';
const styleObj = reactive({
  color: 'red',
  fontSize: '30px',
});
</script>

数组方式则是结合对象形式来使用,在数组中绑定多个样式对象:

<template>
  <div :style="styleArr">绑定内联样式</div>
</template>

<script setup>
import { ref, reactive } from 'vue';
const styleObj = reactive({
  color: 'red',
  fontSize: '30px',
});
const styleObj2 = reactive({
  textDecoration: 'underline',
});
const styleArr = ref([styleObj, styleObj2]);
setTimeout(() => {
  styleArr.value.pop();
}, 3000);
</script>

通过数组方式,可以非常方便地批量操控一组样式,例如添加或移除某个样式对象,这在主题切换、动态皮肤等场景中非常实用。


条件渲染

Q: Vue 中如何根据条件决定元素是否渲染?v-if 和 v-show 有什么区别?

A: Vue 为条件渲染提供了一组内置指令:v-ifv-elsev-else-if。它们和 JavaScript 中的 if/else 逻辑非常相似,上手几乎没有门槛。

<template>
  <div v-if="type === 1">晴天</div>
  <div v-else-if="type === 2">阴天</div>
  <div v-else-if="type === 3">雨天</div>
  <div v-else-if="type === 4">下雪</div>
  <div v-else>不知道什么天气</div>
</template>

<script setup>
import { ref } from 'vue';
const type = ref(1);
setInterval(() => {
  type.value = Math.floor(Math.random() * 5 + 1);
}, 3000);
</script>

如果需要切换多个元素,可以将它们包裹在 <template> 标签中。<template> 本身不会被渲染到 DOM 中,只是一个逻辑容器:

<template>
  <template v-if="type === 1">
    <div>晴天</div>
    <p>要出去旅游</p>
    <p>玩的开心</p>
  </template>
  <template v-else-if="type === 2">
    <div>阴天</div>
    <p>呆在家里吧</p>
    <p>好好看一本书</p>
  </template>
  <template v-else-if="type === 3">
    <div>雨天</div>
    <p>阴天适合睡觉</p>
    <p>好好睡一觉吧</p>
  </template>
  <template v-else-if="type === 4">
    <div>下雪</div>
    <p>下雪啦,我们出去堆雪人吧</p>
    <p>下雪啦,我们出去打雪仗吧</p>
  </template>
  <div v-else>不知道什么天气</div>
</template>

v-if 与 v-show 的区别

除了 v-if,条件渲染还有一个常用指令:v-show。两者的行为有本质区别:

<template>
  <div v-if="isShow">使用 v-if 来做条件渲染</div>
  <div v-show="isShow">使用 v-show 来做条件渲染</div>
</template>

<script setup>
import { ref } from 'vue';
const isShow = ref(true);
setTimeout(() => {
  isShow.value = false;
}, 2000);
</script>

v-show 的切换靠的是 CSS 的 display 属性——当值为 false 时,将 display 设置为 none。关键在于:

特性 v-if v-show
渲染方式 条件为假时不渲染任何 DOM 元素始终被渲染,仅控制 display
切换开销 高(销毁和重建 DOM、事件监听器、子组件) 低(仅 CSS 切换)
初始渲染开销 低(惰性渲染,假时不渲染) 高(无论条件如何都渲染)
适用场景 运行时条件很少改变 需要频繁切换

简单记忆:v-if 是「真的不渲染」,v-show 是「渲染了但藏起来」

列表渲染

Q: v-for 怎么用?key 是做什么的?

A: v-for 用于循环渲染列表,语法为 item in items,和 JavaScript 中的 for..of 循环非常相似。

<template>
  <div>
    <h2>商品列表</h2>
    <ul>
      <li v-for="(product, index) in products" :key="index">
        {{ index + 1 }}{{ product.name }} - {{ product.price }}
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref } from 'vue';
const products = ref([
  { name: '键盘', price: 99.99 },
  { name: '鼠标', price: 59.99 },
  { name: '显示器', price: 299.99 },
]);
</script>

关于 key:在使用 v-for 时,推荐为每个元素指定一个唯一的 key 属性。key 主要用于优化虚拟 DOM 的渲染性能,相当于给虚拟 DOM 节点一个唯一标识。当数据变化时,Vue 可以通过 key 精准地找到对应的节点进行更新,而不是粗暴地重建整个列表。

key 的绑定值应该是一个基础类型(string 或 number),不要使用对象作为 key。

如果使用 <template> 来循环多个元素,key 就挂在 <template> 标签上:

<template>
  <div>
    <h2>商品列表</h2>
    <template v-for="(product, index) in products" :key="index">
      <div>{{ index + 1 }}</div>
      <div>{{ product.name }}</div>
      <div>{{ product.price }}</div>
      <hr />
    </template>
  </div>
</template>

v-for 的更多用法

1. 遍历对象

v-for 不仅可以遍历数组,还可以遍历对象的属性。遍历对象时,参数顺序为:值、键、索引

<template>
  <div v-for="(value, key, index) in stu" :key="index">
    {{ index }} - {{ key }} - {{ value }}
  </div>
</template>

<script setup>
import { reactive } from 'vue';
const stu = reactive({
  name: 'zhangsan',
  age: 18,
  gender: 'male',
  score: 100,
});
</script>

2. 遍历整数范围

v-for 还可以接收一个整数值 n,会从 1 遍历到 n:

<template>
  <div v-for="(value, index) in 10" :key="index">
    {{ value }}
  </div>
</template>

这在需要渲染固定数量的占位元素或骨架屏时非常方便。

3. 嵌套 v-for 与作用域

v-for 同样存在作用域的概念,工作方式与 JavaScript 中的作用域类似——内层可以访问外层的变量:

<template>
  <ul>
    <li v-for="project in projects" :key="project.id">
      <h2>{{ project.name }}</h2>
      <ul>
        <li v-for="task in project.tasks" :key="task.id">
          <h3>{{ task.name }}</h3>
          <ul>
            <li v-for="(subtask, index) in task.subtasks" :key="index">
              {{ project.name }}- {{ task.name }} - {{ subtask }}
            </li>
          </ul>
        </li>
      </ul>
    </li>
  </ul>
</template>

注意最内层的 {{ project.name }}——即使是在第三层嵌套中,它仍然可以访问到最外层 v-forproject 变量。

4. 不要同时使用 v-if 和 v-for

官方明确指出:不要在同一个元素上同时使用 v-if 和 v-for,因为两者的优先级不明显,容易导致意外行为。


<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

正确的做法是在外层包装一层 <template>


<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

数组变更侦测

谈到 v-for 就离不开数据的动态更新。Vue 的响应式系统能够侦测到数组的变化并自动重新渲染,但有一个重要前提需要了解。

数组方法分为两大类:

变更方法——会修改原数组,这些方法的调用会自动触发视图更新

  • push / pop
  • shift / unshift
  • splice
  • sort
  • reverse
setTimeout(() => {
  projects.value.push({
    id: 3,
    name: '这是一个大项目',
    tasks: [ /* ... */ ],
  });
}, 3000);

非变更方法——返回一个新数组,不会修改原数组:

  • filter
  • concat
  • slice
  • map

使用这些方法时,需要用返回值替换原来的值才能触发响应式更新:

// items 是一个数组的 ref
items.value = items.value.filter((item) => item.message.match(/Foo/));

这一点在初学 Vue 时容易忽略——调用 filter 后页面「没反应」,往往就是因为忘了将结果重新赋值给原 ref。


小结

本文涵盖了 Vue3 中三个非常常用的模板功能:

  • class/style 绑定:对象语法适合「有条件的样式切换」,数组语法适合「动态组合多个样式」。当逻辑复杂时,优先使用计算属性返回样式对象。
  • 条件渲染:v-if 控制是否渲染 DOM(适合切换不频繁的场景),v-show 控制 display 属性(适合频繁切换)。两者各有优劣,按场景选择。
  • 列表渲染:v-for 支持遍历数组、对象、整数范围。始终记得绑定唯一 key,不要在同一个元素上混用 v-if 和 v-for,使用非变更数组方法后要用返回值替换原值。

掌握了这些技巧,日常开发中处理动态 UI 就会游刃有余。