侧边栏壁纸
博主头像
洋洋的技术笔记

行动起来,活在当下

  • 累计撰写 11 篇文章
  • 累计创建 2 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录
VUE

05-条件渲染与列表渲染

洋洋的技术笔记
2026-11-01 / 0 评论 / 0 点赞 / 2 阅读 / 0 字 / 正在检测是否收录...

条件渲染与列表渲染

页面不是静态的:数据决定内容是否出现,列表决定内容如何重复。掌握条件渲染与列表渲染,就掌握了动态页面的基本骨架。

在上一篇文章中,我们学习了计算属性与侦听器。本文将进一步解决两个高频问题:如何根据状态切换页面内容,以及如何正确、高效地渲染一组数据。

写作约定:本系列以 Vue 3 <script setup> 为主要写法。示例默认使用 Vue 3.2+,重点关注实际开发中的选择依据与常见陷阱。


一、条件渲染:让数据决定页面结构

条件渲染并不只是“显示或隐藏一个元素”。它还会影响到 DOM 是否创建、组件是否初始化,以及切换时需要付出多少性能成本。

1.1 v-if、v-else-if 与 v-else

v-if 会根据表达式的真假决定是否渲染元素。多个分支可以组合成完整的条件链:

<template>
  <p v-if="status === 'loading'">正在加载...</p>
  <p v-else-if="status === 'error'">加载失败,请稍后重试</p>
  <p v-else-if="status === 'empty'">暂无数据</p>
  <p v-else>数据加载完成</p>
</template>

<script setup>
import { ref } from 'vue'
const status = ref('loading')
</script>

这些分支必须紧跟在一起,中间不能插入其他元素,否则 v-else-if 或 v-else 将无法识别前面的 v-if。

当一组元素需要整体控制时,可以把指令放在 <template> 上:

<template>
  <button v-if="!isLoggedIn" @click="isLoggedIn = true">登录</button>
  <template v-else>
    <h2>欢迎回来</h2>
    <p>你可以继续管理个人资料。</p>
    <button @click="isLoggedIn = false">退出登录</button>
  </template>
</template>

<script setup>
import { ref } from 'vue'
const isLoggedIn = ref(false)
</script>

这里的 <template> 只是分组容器,不会作为额外节点出现在最终 DOM 中。

1.2 v-show:保留 DOM,只切换显示状态

v-show 不论条件真假都会渲染元素,条件为假时通过 CSS 的 display: none 隐藏:

<template>
  <button @click="visible = !visible">切换帮助信息</button>
  <div v-show="visible" class="help-panel">
    这里是经常展开和收起的帮助内容。
  </div>
</template>

<script setup>
import { ref } from 'vue'
const visible = ref(true)
</script>

v-show 不支持在 <template> 上使用,也不能与 v-else 组成条件链。

1.3 v-if 与 v-show 如何选择

对比项

v-if

v-show

条件为假时

不渲染或移除 DOM

保留 DOM,设置 display: none

初始渲染

惰性,只有条件为真才创建

无论条件真假都会创建

切换成本

需要创建或销毁节点

只修改 CSS,通常较低

组件生命周期

会触发挂载与卸载

不会因显示切换而重新挂载

典型场景

权限、加载状态、很少变化的分支

弹层、Tab、频繁展开收起

可以记住一个简单原则:很少切换用 v-if,频繁切换用 v-show,初始开销和组件生命周期也要一起考虑。

1.4 使用 key 区分不同分支

Vue 会尽量复用更新前后的同类型元素。如果两个分支结构相似,但业务含义不同,应该用 key 明确区分:

<template>
  <input
    v-if="loginType === 'username'"
    key="username-input"
    placeholder="输入用户名"
  >
  <input
    v-else
    key="email-input"
    placeholder="输入邮箱"
  >
  <button @click="toggleLoginType">切换登录方式</button>
</template>

<script setup>
import { ref } from 'vue'
const loginType = ref('username')
const toggleLoginType = () => {
  loginType.value = loginType.value === 'username' ? 'email' : 'username'
}
</script>

加上不同的 key 后,Vue 会替换输入框,而不是继续复用旧节点。这能避免旧输入内容、焦点或组件内部状态被意外保留。


二、列表渲染:用 v-for 处理一组数据

v-for 基于源数据重复渲染元素。基本语法是 item in items,也可以使用 of 代替 in,使其更接近 JavaScript 迭代语法。

2.1 遍历数组

<template>
  <ul>
    <li v-for="(book, index) in books" :key="book.id">
      {{ index + 1 }}.《{{ book.title }}》
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue'
const books = ref([
  { id: 1, title: 'Vue.js设计与实现' },
  { id: 2, title: 'JavaScript高级程序设计' },
  { id: 3, title: '深入浅出Vue.js' }
])
</script>

book 是当前项,index 是从 0 开始的索引。业务渲染应优先使用当前项中的稳定 ID 作为 key。

2.2 遍历对象与整数

遍历对象时,可以依次取得值、键和索引:

<template>
  <ul>
    <li v-for="(value, key) in user" :key="key">
      {{ key }}:{{ value }}
    </li>
  </ul>
  <div>
    <span v-for="page in 5" :key="page" class="page">{{ page }}</span>
  </div>
</template>

<script setup>
const user = {
  name: '张三',
  role: '前端工程师',
  city: '济南'
}
</script>

v-for="page in 5" 会从 1 开始,依次得到 1 到 5,而不是 0 到 4。

2.3 在 template 上渲染多个元素

当每个数据项对应多个兄弟元素时,可以使用 <template v-for>,并把 key 放在 <template> 上:

<template>
  <dl>
    <template v-for="item in descriptions" :key="item.id">
      <dt>{{ item.term }}</dt>
      <dd>{{ item.description }}</dd>
    </template>
  </dl>
</template>

<script setup>
const descriptions = [
  { id: 1, term: 'v-if', description: '按条件创建或销毁内容' },
  { id: 2, term: 'v-for', description: '根据数据重复渲染内容' }
]
</script>

这样既不需要多余的包裹节点,也能让每一组 dt、dd 拥有明确的身份。


三、key 的真正作用:帮助 Vue 识别节点

key 不是普通的 HTML 属性,而是 Vue 虚拟 DOM Diff 算法识别节点的依据。它回答的是:更新前后,哪些节点仍然代表同一个数据项?

3.1 为什么推荐稳定且唯一的 ID

假设原列表是 [A, B, C],现在在开头插入 D。如果使用索引作为 key,各项的 key 与数据对应关系就会变化:

更新前:A(key=0) B(key=1) C(key=2)
更新后:D(key=0) A(key=1) B(key=2) C(key=3)

Vue 可能把原来的 A 节点更新成 D,把 B 更新成 A,而不是简单复用 A、B、C。普通文本通常仍能正确更新,但输入框、子组件内部状态或焦点可能出现错位。

<template>
  <!-- 推荐:ID 在数据生命周期内保持稳定 -->
  <li v-for="todo in todos" :key="todo.id">{{ todo.title }}</li>
</template>

索引并非绝对不能用。对于不会排序、过滤、插入或删除,且节点没有独立状态的静态列表,索引通常可以接受。一旦列表会动态变化,就应使用业务 ID。

3.2 key 应该放在哪里

普通元素直接把 key 放在元素上;组件列表把 key 放在组件上;<template v-for> 则把 key 放在 <template> 上:

<template>
  <TodoItem v-for="todo in todos" :key="todo.id" :todo="todo" />
</template>

不要把 key 当成列表样式或业务标识传给子组件,它的主要职责是辅助 Vue 正确复用节点。


四、条件渲染与列表渲染的组合

4.1 不建议在同一元素上混用 v-if 与 v-for

在 Vue 3 中,同一元素上的 v-if 优先级高于 v-for,因此 v-if 往往无法取得当前循环项。即使改成依赖外部条件,混合写法也会让意图变得不清晰。

不推荐:

<template>
  <li v-for="user in users" v-if="user.active" :key="user.id">
    {{ user.name }}
  </li>
</template>

更推荐先通过计算属性得到需要展示的数据:

<template>
  <li v-for="user in activeUsers" :key="user.id">
    {{ user.name }}
  </li>
</template>

<script setup>
import { computed, ref } from 'vue'
const users = ref([
  { id: 1, name: '张三', active: true },
  { id: 2, name: '李四', active: false },
  { id: 3, name: '王五', active: true }
])
const activeUsers = computed(() => users.value.filter(user => user.active))
</script>

这样条件逻辑与循环结构各自独立,也更容易测试和维护。

4.2 处理加载、空列表与正常数据

实际页面通常不只有“显示”和“隐藏”两种状态,还应考虑加载、失败和空结果:

<template>
  <p v-if="loading">正在加载用户...</p>
  <p v-else-if="errorMessage">{{ errorMessage }}</p>
  <ul v-else-if="users.length">
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
  <p v-else>暂无用户</p>
</template>

<script setup>
import { ref } from 'vue'
const loading = ref(false)
const errorMessage = ref('')
const users = ref([])
</script>

把状态分支写完整,可以避免页面在请求过程中或没有数据时出现空白。


五、列表更新与性能注意事项

Vue 3 能侦测 push、pop、splice、sort、reverse 等数组变更,也能侦测通过索引赋值和修改 length。需要关注的是更新方式是否符合业务意图,而不是沿用 Vue 2 的旧限制。

5.1 修改原数组与替换数组

两种方式都可以触发更新:

<script setup>
import { ref } from 'vue'
const todos = ref([{ id: 1, title: '学习v-for' }])
// 修改原数组,适合局部增删
const addTodo = () => {
  todos.value.push({ id: 2, title: '学习key' })
}
// 替换数组,适合过滤、排序和不可变更新
const removeTodo = (id) => {
  todos.value = todos.value.filter(todo => todo.id !== id)
}
</script>

排序时要特别注意:sort() 会修改原数组。如果只想改变展示顺序,不想改变源数据,应先复制再排序:

<script setup>
import { computed, ref } from 'vue'
const products = ref([
  { id: 1, name: '键盘', price: 199 },
  { id: 2, name: '鼠标', price: 99 }
])
const productsByPrice = computed(() => {
  return [...products.value].sort((a, b) => a.price - b.price)
})
</script>

5.2 不要只追求“复用越多越好”

正确的 key 能减少不必要的 DOM 操作,但 key 的首要价值是保证渲染结果和组件状态正确。如果业务上需要强制重建一个组件,也可以主动改变它的 key;不要为了追求复用而让不同数据项共享错误状态。


六、综合实战:商品搜索、筛选与排序

下面把条件渲染、列表渲染和计算属性组合起来,实现一个完整的商品列表示例:

<template>
  <section class="product-panel">
    <h2>商品列表</h2>
    <div class="toolbar">
      <input v-model.trim="keyword" placeholder="搜索商品名称">
      <label>
        <input v-model="stockOnly" type="checkbox">
        仅看有货
      </label>
      <select v-model="sortType">
        <option value="default">默认排序</option>
        <option value="priceAsc">价格从低到高</option>
        <option value="priceDesc">价格从高到低</option>
      </select>
    </div>
    <p v-if="displayProducts.length === 0" class="empty">
      没有找到符合条件的商品
    </p>
    <ul v-else class="product-list">
      <li v-for="product in displayProducts" :key="product.id">
        <div>
          <strong>{{ product.name }}</strong>
          <span>¥{{ product.price }}</span>
        </div>
        <span :class="product.stock > 0 ? 'in-stock' : 'out-of-stock'">
          {{ product.stock > 0 ? `库存 ${product.stock}` : '暂时缺货' }}
        </span>
      </li>
    </ul>
  </section>
</template>

<script setup>
import { computed, ref } from 'vue'
const products = ref([
  { id: 1, name: '机械键盘', price: 299, stock: 12 },
  { id: 2, name: '无线鼠标', price: 129, stock: 0 },
  { id: 3, name: '显示器', price: 1099, stock: 5 },
  { id: 4, name: '扩展坞', price: 399, stock: 8 }
])
const keyword = ref('')
const stockOnly = ref(false)
const sortType = ref('default')
const filteredProducts = computed(() => {
  const searchText = keyword.value.toLowerCase()
  return products.value.filter(product => {
    const matchesKeyword = product.name.toLowerCase().includes(searchText)
    const matchesStock = !stockOnly.value || product.stock > 0
    return matchesKeyword && matchesStock
  })
})
const displayProducts = computed(() => {
  const result = [...filteredProducts.value]
  if (sortType.value === 'priceAsc') {
    return result.sort((a, b) => a.price - b.price)
  }
  if (sortType.value === 'priceDesc') {
    return result.sort((a, b) => b.price - a.price)
  }
  return result
})
</script>

<style scoped>
.product-panel {
  max-width: 680px;
  margin: 24px auto;
}
.toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  margin-bottom: 16px;
}
.product-list {
  padding: 0;
  list-style: none;
}
.product-list li {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 0;
  border-bottom: 1px solid #e5e7eb;
}
.product-list li div {
  display: flex;
  gap: 16px;
}
.empty {
  padding: 32px;
  color: #6b7280;
  text-align: center;
}
.in-stock {
  color: #059669;
}
.out-of-stock {
  color: #dc2626;
}
</style>

这个例子中有三个值得保留的思路:

  1. 过滤与排序分开:filteredProducts 负责筛选,displayProducts 负责展示顺序。

  2. 不修改源数组:排序前先复制,避免展示逻辑意外改变 products。

  3. 空状态明确:没有符合条件的数据时给出提示,而不是只渲染一个空列表。


七、常见问题自查

问题

推荐处理

频繁显示、隐藏内容

优先考虑 v-show

权限或低频页面分支

使用 v-if

多个元素共享一个条件

使用 <template v-if> 或 <template v-for>

列表会增删、排序或过滤

使用稳定且唯一的业务 ID 作为 key

需要按条件过滤列表

先使用计算属性,再执行 v-for

排序后源数组被改变

使用 [...items].sort()

页面没有数据时空白

补充加载、失败和空结果分支


八、总结

条件渲染和列表渲染看似是基础语法,却直接影响页面的正确性、组件状态和渲染性能。本文最重要的几点是:

  1. v-if 控制是否创建内容,v-show 控制是否显示内容。

  2. key 的核心作用是帮助 Vue 识别节点,动态列表优先使用稳定 ID。

  3. 不在同一元素上混用 v-if 和 v-for,优先用计算属性表达过滤逻辑。

  4. 排序、过滤时明确是否修改源数据,避免隐式副作用。

  5. 完整页面应同时考虑加载、失败、空结果和正常数据状态。

下一站预告

下一篇《事件处理与表单输入绑定》将继续完善交互能力,深入讲解事件修饰符、按键处理、v-model 以及常见表单场景。


作者:洋洋技术笔记 发布日期:2026-11-01 系列:Vue.js从入门到精通 - 第5篇

0

评论区