第一部分:Vue 3 基础入门

第10集:条件渲染与列表渲染入门

在Vue应用中,条件渲染和列表渲染是非常常见的操作。Vue提供了简洁、高效的指令来处理这些场景。在本集中,我们将学习Vue 3的条件渲染和列表渲染基础,包括v-ifv-showv-for等指令的使用方法和最佳实践。

10.1 条件渲染

条件渲染是指根据条件来决定是否渲染某个元素或组件。Vue提供了v-ifv-show两个指令来实现条件渲染。

10.1.1 v-if 指令

v-if指令用于条件性地渲染元素或组件。当条件为真时,元素或组件会被渲染;当条件为假时,元素或组件会被销毁。

基本使用

<template>
  <div>
    <h1>条件渲染</h1>
    <button @click="toggleShow">切换显示</button>
    
    <div v-if="isShow">
      <h2>这是v-if渲染的内容</h2>
      <p>条件为真时显示,为假时销毁</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const isShow = ref(true)

function toggleShow() {
  isShow.value = !isShow.value
}
</script>

10.1.2 v-else-if 和 v-else

v-if可以与v-else-ifv-else配合使用,实现多条件分支渲染。

示例

<template>
  <div>
    <h1>多条件渲染</h1>
    <input type="number" v-model.number="score" placeholder="输入分数">
    
    <div v-if="score >= 90">
      <h2>优秀</h2>
      <p>你的分数是 {{ score }},非常棒!</p>
    </div>
    <div v-else-if="score >= 80">
      <h2>良好</h2>
      <p>你的分数是 {{ score }},继续努力!</p>
    </div>
    <div v-else-if="score >= 60">
      <h2>及格</h2>
      <p>你的分数是 {{ score }},刚刚及格,需要加油!</p>
    </div>
    <div v-else-if="score >= 0">
      <h2>不及格</h2>
      <p>你的分数是 {{ score }},需要更加努力!</p>
    </div>
    <div v-else>
      <h2>请输入有效分数</h2>
      <p>分数必须是大于等于0的数字</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const score = ref(0)
</script>

10.1.3 v-show 指令

v-show指令用于条件性地显示元素或组件。无论条件是否为真,元素或组件都会被渲染,只是通过CSS的display属性来控制显示或隐藏。

示例

<template>
  <div>
    <h1>v-show 示例</h1>
    <button @click="toggleShow">切换显示</button>
    
    <div v-show="isShow">
      <h2>这是v-show渲染的内容</h2>
      <p>无论条件真假,元素都会被渲染,只是通过CSS控制显示/隐藏</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const isShow = ref(true)

function toggleShow() {
  isShow.value = !isShow.value
}
</script>

10.1.4 v-if vs v-show

特性 v-if v-show
渲染方式 条件为真时渲染,为假时销毁 始终渲染,通过CSS display控制
性能开销 切换时开销大 初始渲染开销大
适用场景 条件不频繁切换 条件频繁切换
支持template
支持v-else
元素生命周期 触发完整的生命周期 不触发生命周期

使用建议

  • 如果条件不频繁切换,使用v-if
  • 如果条件频繁切换,使用v-show
  • 如果需要使用v-elsetemplate,使用v-if

10.1.5 在 template 元素上使用 v-if

我们可以在template元素上使用v-if,这样可以条件渲染一组元素,而不需要额外的DOM元素来包裹它们。

示例

<template>
  <div>
    <h1>在template上使用v-if</h1>
    <button @click="toggleShow">切换显示</button>
    
    <template v-if="isShow">
      <h2>标题</h2>
      <p>第一段内容</p>
      <p>第二段内容</p>
      <ul>
        <li>列表项1</li>
        <li>列表项2</li>
        <li>列表项3</li>
      </ul>
    </template>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const isShow = ref(true)

function toggleShow() {
  isShow.value = !isShow.value
}
</script>

10.2 列表渲染

列表渲染是指根据数据列表来渲染一组元素或组件。Vue提供了v-for指令来实现列表渲染。

10.2.1 v-for 基本使用

v-for指令用于遍历数组或对象,并为每个元素或属性渲染一个元素或组件。

遍历数组

<template>
  <div>
    <h1>列表渲染</h1>
    
    <h2>遍历数组</h2>
    <ul>
      <li v-for="(item, index) in items" :key="item.id">
        {{ index + 1 }}. {{ item.name }} - {{ item.price }}元
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const items = ref([
  { id: 1, name: '苹果', price: 5.5 },
  { id: 2, name: '香蕉', price: 3.0 },
  { id: 3, name: '橙子', price: 4.0 },
  { id: 4, name: '草莓', price: 10.0 },
  { id: 5, name: '葡萄', price: 8.0 }
])
</script>

遍历对象

<template>
  <div>
    <h2>遍历对象</h2>
    <ul>
      <li v-for="(value, key, index) in user" :key="key">
        {{ index + 1 }}. {{ key }}: {{ value }}
      </li>
    </ul>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const user = reactive({
  name: '张三',
  age: 25,
  email: 'zhangsan@example.com',
  address: '北京市朝阳区'
})
</script>

遍历数字

<template>
  <div>
    <h2>遍历数字</h2>
    <ul>
      <li v-for="n in 5" :key="n">
        第 {{ n }} 项
      </li>
    </ul>
  </div>
</template>

10.2.2 key 属性

在使用v-for时,我们需要为每个渲染的元素提供一个唯一的key属性。key属性的作用是帮助Vue识别每个元素的身份,从而高效地更新DOM。

为什么需要key?

  • Vue使用key来跟踪每个节点的身份
  • 当列表数据变化时,Vue会根据key来决定如何更新DOM
  • 正确使用key可以提高渲染性能,避免不必要的DOM操作

key的选择

  • 优先使用唯一的ID作为key,如数据库中的主键
  • 避免使用索引作为key,尤其是当列表涉及排序、过滤、删除等操作时
  • 对于简单的静态列表,可以使用索引作为key

示例

<!-- 推荐:使用唯一ID作为key -->
<li v-for="item in items" :key="item.id">
  {{ item.name }}
</li>

<!-- 不推荐:使用索引作为key -->
<li v-for="(item, index) in items" :key="index">
  {{ item.name }}
</li>

10.2.3 数组更新检测

Vue能够检测到数组的以下变更方法,并自动触发视图更新:

  • push():向数组末尾添加元素
  • pop():删除数组末尾的元素
  • shift():删除数组第一个元素
  • unshift():向数组开头添加元素
  • splice():插入、删除或替换数组元素
  • sort():对数组进行排序
  • reverse():反转数组

示例

<template>
  <div>
    <h1>数组更新检测</h1>
    
    <ul>
      <li v-for="item in items" :key="item.id">
        {{ item.name }}
      </li>
    </ul>
    
    <div class="buttons">
      <button @click="addItem">添加元素</button>
      <button @click="removeItem">删除元素</button>
      <button @click="sortItems">排序</button>
      <button @click="reverseItems">反转</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const items = ref([
  { id: 1, name: '苹果' },
  { id: 2, name: '香蕉' },
  { id: 3, name: '橙子' }
])

function addItem() {
  items.value.push({ id: Date.now(), name: '新水果' })
}

function removeItem() {
  items.value.pop()
}

function sortItems() {
  items.value.sort((a, b) => a.name.localeCompare(b.name))
}

function reverseItems() {
  items.value.reverse()
}
</script>

<style scoped>
.buttons {
  margin-top: 20px;
}

button {
  margin-right: 10px;
  padding: 8px 16px;
  background-color: #42b883;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

注意事项

  • Vue不能检测到通过索引直接修改数组元素,如items[0] = newItem
  • Vue不能检测到修改数组长度,如items.length = 0

解决方法

  • 使用Vue.set(items, index, newValue)this.$set(items, index, newValue)(Vue 2)
  • 使用items.splice(index, 1, newValue)
  • 使用扩展运算符创建新数组:items.value = [...items.value.slice(0, index), newValue, ...items.value.slice(index + 1)]

10.2.4 在 template 上使用 v-for

我们可以在template元素上使用v-for,这样可以渲染一组元素,而不需要额外的DOM元素来包裹它们。

示例

<template>
  <div>
    <h1>在template上使用v-for</h1>
    
    <template v-for="item in items" :key="item.id">
      <h2>{{ item.title }}</h2>
      <p>{{ item.content }}</p>
      <hr>
    </template>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const items = ref([
  { id: 1, title: '标题1', content: '内容1' },
  { id: 2, title: '标题2', content: '内容2' },
  { id: 3, title: '标题3', content: '内容3' }
])
</script>

10.2.5 v-for 与 v-if 一起使用

在Vue 3中,不建议在同一个元素上同时使用v-forv-if,因为v-if的优先级高于v-for,这会导致v-if无法访问到v-for的变量。

不推荐的用法

<!-- 不推荐:v-if无法访问到v-for的变量 -->
<li v-for="item in items" :key="item.id" v-if="item.isActive">
  {{ item.name }}
</li>

推荐的用法

  1. 使用计算属性过滤数据
  2. template上使用v-for,然后在内部元素上使用v-if

示例

<template>
  <div>
    <h1>v-for与v-if一起使用</h1>
    
    <!-- 方法1:使用计算属性 -->
    <h2>方法1:使用计算属性</h2>
    <ul>
      <li v-for="item in activeItems" :key="item.id">
        {{ item.name }}
      </li>
    </ul>
    
    <!-- 方法2:在template上使用v-for -->
    <h2>方法2:在template上使用v-for</h2>
    <ul>
      <template v-for="item in items" :key="item.id">
        <li v-if="item.isActive">
          {{ item.name }}
        </li>
      </template>
    </ul>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const items = ref([
  { id: 1, name: '苹果', isActive: true },
  { id: 2, name: '香蕉', isActive: false },
  { id: 3, name: '橙子', isActive: true },
  { id: 4, name: '草莓', isActive: false },
  { id: 5, name: '葡萄', isActive: true }
])

// 方法1:使用计算属性过滤数据
const activeItems = computed(() => {
  return items.value.filter(item => item.isActive)
})
</script>

10.3 条件渲染与列表渲染的综合示例

让我们创建一个综合示例,结合条件渲染和列表渲染来实现一个简单的商品列表,包括筛选、排序、添加、删除等功能。

示例

<template>
  <div class="product-app">
    <h1>商品列表</h1>
    
    <!-- 添加商品 -->
    <div class="add-product">
      <h2>添加商品</h2>
      <input 
        type="text" 
        v-model="newProduct.name" 
        placeholder="商品名称"
      >
      <input 
        type="number" 
        v-model.number="newProduct.price" 
        placeholder="商品价格"
      >
      <button @click="addProduct">添加</button>
    </div>
    
    <!-- 筛选和排序 -->
    <div class="filter-sort">
      <h2>筛选和排序</h2>
      <div>
        <label for="filter">筛选:</label>
        <select id="filter" v-model="filter">
          <option value="all">全部</option>
          <option value="expensive">价格 > 5元</option>
          <option value="cheap">价格 <= 5元</option>
        </select>
      </div>
      <div>
        <label for="sort">排序:</label>
        <select id="sort" v-model="sortBy">
          <option value="name">按名称</option>
          <option value="price">按价格</option>
        </select>
        <button @click="toggleSortOrder">
          {{ sortOrder === 'asc' ? '升序' : '降序' }}
        </button>
      </div>
    </div>
    
    <!-- 商品列表 -->
    <div class="product-list">
      <h2>商品列表</h2>
      
      <!-- 条件渲染:当没有商品时显示提示 -->
      <div v-if="filteredProducts.length === 0" class="no-products">
        <p>没有找到商品</p>
      </div>
      
      <!-- 列表渲染:显示商品列表 -->
      <table v-else>
        <thead>
          <tr>
            <th>序号</th>
            <th>名称</th>
            <th>价格</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(product, index) in filteredProducts" :key="product.id">
            <td>{{ index + 1 }}</td>
            <td>{{ product.name }}</td>
            <td>{{ product.price }}元</td>
            <td>
              <button @click="deleteProduct(product.id)">删除</button>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
    
    <!-- 统计信息 -->
    <div class="stats">
      <h2>统计信息</h2>
      <p>商品总数:{{ products.length }}</p>
      <p>显示数量:{{ filteredProducts.length }}</p>
      <p>平均价格:{{ averagePrice.toFixed(2) }}元</p>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

// 商品列表
const products = ref([
  { id: 1, name: '苹果', price: 5.5 },
  { id: 2, name: '香蕉', price: 3.0 },
  { id: 3, name: '橙子', price: 4.0 },
  { id: 4, name: '草莓', price: 10.0 },
  { id: 5, name: '葡萄', price: 8.0 }
])

// 新商品
const newProduct = ref({
  name: '',
  price: 0
})

// 筛选条件
const filter = ref('all')

// 排序条件
const sortBy = ref('name')
const sortOrder = ref('asc')

// 计算属性:筛选和排序后的商品列表
const filteredProducts = computed(() => {
  let result = [...products.value]
  
  // 筛选
  if (filter.value === 'expensive') {
    result = result.filter(product => product.price > 5)
  } else if (filter.value === 'cheap') {
    result = result.filter(product => product.price <= 5)
  }
  
  // 排序
  result.sort((a, b) => {
    if (sortBy.value === 'name') {
      return sortOrder.value === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
    } else {
      return sortOrder.value === 'asc' ? a.price - b.price : b.price - a.price
    }
  })
  
  return result
})

// 计算属性:平均价格
const averagePrice = computed(() => {
  if (products.value.length === 0) return 0
  const total = products.value.reduce((sum, product) => sum + product.price, 0)
  return total / products.value.length
})

// 方法:添加商品
function addProduct() {
  if (newProduct.value.name && newProduct.value.price > 0) {
    products.value.push({
      id: Date.now(),
      name: newProduct.value.name,
      price: newProduct.value.price
    })
    // 重置表单
    newProduct.value.name = ''
    newProduct.value.price = 0
  }
}

// 方法:删除商品
function deleteProduct(id) {
  products.value = products.value.filter(product => product.id !== id)
}

// 方法:切换排序顺序
function toggleSortOrder() {
  sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
}
</script>

<style scoped>
.product-app {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.add-product, .filter-sort, .product-list, .stats {
  margin-bottom: 30px;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.add-product input {
  margin-right: 10px;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

button {
  padding: 8px 16px;
  background-color: #42b883;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-right: 10px;
}

.filter-sort div {
  margin-bottom: 10px;
}

select {
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  margin-right: 10px;
}

.product-list table {
  width: 100%;
  border-collapse: collapse;
}

.product-list th, .product-list td {
  padding: 10px;
  border: 1px solid #ddd;
  text-align: left;
}

.product-list th {
  background-color: #f0f0f0;
}

.no-products {
  text-align: center;
  padding: 20px;
  background-color: #f9f9f9;
  border: 1px solid #eee;
  border-radius: 4px;
}

.stats p {
  margin: 5px 0;
}
</style>

10.4 条件渲染与列表渲染的最佳实践

  1. 合理选择条件渲染指令

    • 条件不频繁切换时使用v-if
    • 条件频繁切换时使用v-show
    • 需要使用v-elsetemplate时使用v-if
  2. 正确使用key属性

    • 优先使用唯一ID作为key
    • 避免使用索引作为key,尤其是当列表涉及排序、过滤、删除等操作时
    • 确保key的唯一性
  3. 避免在同一元素上同时使用v-for和v-if

    • 使用计算属性过滤数据
    • template上使用v-for,然后在内部元素上使用v-if
  4. 优化长列表性能

    • 考虑使用虚拟滚动技术(如vue-virtual-scroller)
    • 合理使用v-memo指令缓存组件
    • 避免在列表中进行复杂的计算
  5. 保持模板简洁

    • 将复杂的条件判断逻辑提取到计算属性或方法中
    • 将复杂的列表项拆分为单独的组件
    • 避免在模板中写过多的JavaScript代码
  6. 处理空状态

    • 为条件渲染和列表渲染添加适当的空状态提示
    • 提供友好的用户反馈

本集小结

在本集中,我们学习了Vue 3的条件渲染和列表渲染基础:

  • 条件渲染

    • 使用v-ifv-else-ifv-else实现条件渲染
    • 使用v-show实现条件显示
    • 了解了v-ifv-show的区别和适用场景
    • 学习了在template上使用v-if
  • 列表渲染

    • 使用v-for遍历数组、对象和数字
    • 了解了key属性的重要性和正确用法
    • 学习了数组更新检测和注意事项
    • 学习了在template上使用v-for
    • 了解了如何正确地将v-forv-if一起使用
  • 综合示例

    • 实现了一个完整的商品列表应用
    • 结合了条件渲染和列表渲染
    • 实现了添加、删除、筛选、排序等功能
    • 使用了计算属性来优化代码
  • 最佳实践

    • 合理选择条件渲染指令
    • 正确使用key属性
    • 避免在同一元素上同时使用v-forv-if
    • 优化长列表性能
    • 保持模板简洁
    • 处理空状态

条件渲染和列表渲染是Vue应用开发中的核心功能,掌握好这些知识可以帮助我们创建复杂、交互丰富的应用。到目前为止,我们已经完成了Vue 3基础入门部分的学习,包括环境搭建、项目结构、单文件组件、模板语法、响应式数据、事件处理、表单绑定、条件渲染和列表渲染等内容。

在下一部分中,我们将学习Vue 3的组件化开发基础,包括组件定义、组件通信、插槽等内容。让我们继续深入学习Vue 3!

« 上一篇 事件处理与表单绑定基础 下一篇 » 组件化思想与组件定义