<返回目录     Powered by claud/xia兄

第4课: 条件渲染与列表渲染

深入理解Vue3条件渲染机制与列表渲染优化,掌握性能调优技巧

📚 学习目标

🎯 条件渲染:v-if vs v-show深度解析

条件渲染是Vue中控制元素显示/隐藏的核心功能,v-if和v-show虽然功能相似,但实现机制和适用场景完全不同。

🔬 渲染机制原理

v-if:条件性渲染 vs v-show:CSS显示控制

v-if工作原理

  1. 编译阶段:根据条件生成不同的渲染分支
  2. 条件为真:渲染元素并插入DOM
  3. 条件为假:从DOM中移除元素,销毁组件实例
  4. 切换开销:每次切换都会重新创建/销毁

v-show工作原理

  1. 编译阶段:始终渲染元素到DOM
  2. 条件为真:设置display: block
  3. 条件为假:设置display: none
  4. 切换开销:只修改CSS属性,不重新渲染

🔍 v-if条件渲染详解

v-if指令用于真正的条件渲染,条件为真时元素才会被渲染到DOM中。

<template>
  <div>
    <h1 v-if="awesome">Vue is awesome!</h1>
    <h1 v-else>Oh no</h1>

    <div v-if="type === 'A'">A</div>
    <div v-else-if="type === 'B'">B</div>
    <div v-else-if="type === 'C'">C</div>
    <div v-else>Not A/B/C</div>
  </div>
</template>

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

const awesome = ref(true)
const type = ref('A')
</script>

🔧 template上的v-if

使用<template>标签可以在不添加额外DOM元素的情况下条件渲染多个元素。

<template>
  <div>
    <!-- 使用template包裹多个元素 -->
    <template v-if="userLoggedIn">
      <h1>欢迎回来,{{ userName }}!</h1>
      <nav>
        <a href="/profile">个人资料</a>
        <a href="/settings">设置</a>
        <button @click="logout">退出登录</button>
      </nav>
    </template>
    
    <!-- 未登录状态 -->
    <template v-else>
      <h1>请先登录</h1>
      <button @click="login">登录</button>
      <button @click="register">注册</button>
    </template>
    
    <!-- 复杂条件判断 -->
    <template v-if="loading">
      <div class="loading">加载中...</div>
    </template>
    <template v-else-if="error">
      <div class="error">{{ errorMessage }}</div>
    </template>
    <template v-else>
      <div class="content">内容加载完成</div>
    </template>
  </div>
</template>

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

const userLoggedIn = ref(true)
const userName = ref('张三')
const loading = ref(false)
const error = ref(false)
const errorMessage = ref('')

function login() {
  userLoggedIn.value = true
  userName.value = '新用户'
}

function logout() {
  userLoggedIn.value = false
  userName.value = ''
}

function register() {
  console.log('注册功能')
}
</script>
template标签的优势:

👁️ v-show条件显示

v-show通过CSS的display属性控制元素的显示和隐藏,元素始终存在于DOM中。

🔍 v-show实时演示

<template>
  <div>
    <h1 v-show="isVisible">Hello Vue3!</h1>
    
    <div style="margin: 20px 0;">
      <button @click="toggleVisibility">
        {{ isVisible ? '隐藏' : '显示' }}标题
      </button>
      
      <button @click="toggleAnimation">
        {{ useAnimation ? '关闭' : '开启' }}动画
      </button>
    </div>
    
    <!-- 复杂组件的显示控制 -->
    <div v-show="showAdvancedOptions" class="advanced-options">
      <h3>高级选项</h3>
      <div>
        <label>选项1: <input type="checkbox" v-model="option1"></label>
        <label>选项2: <input type="checkbox" v-model="option2"></label>
        <label>选项3: <input type="checkbox" v-model="option3"></label>
      </div>
    </div>
    
    <button @click="showAdvancedOptions = !showAdvancedOptions">
      {{ showAdvancedOptions ? '隐藏' : '显示' }}高级选项
    </button>
    
    <!-- 显示当前状态 -->
    <p>标题显示状态: {{ isVisible ? '显示' : '隐藏' }}</p>
    <p>高级选项状态: {{ showAdvancedOptions ? '显示' : '隐藏' }}</p>
  </div>
</template>

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

const isVisible = ref(true)
const useAnimation = ref(true)
const showAdvancedOptions = ref(false)
const option1 = ref(false)
const option2 = ref(false)
const option3 = ref(false)

function toggleVisibility() {
  isVisible.value = !isVisible.value
}

function toggleAnimation() {
  useAnimation.value = !useAnimation.value
}
</script>

<style scoped>
.advanced-options {
  border: 1px solid #ccc;
  padding: 15px;
  margin: 10px 0;
  border-radius: 5px;
  background-color: #f9f9f9;
}

h1 {
  transition: all 0.3s ease;
}

h1[v-show="false"] {
  opacity: 0;
  transform: translateY(-20px);
}
</style>

📊 v-if vs v-show:深度对比与选择指南

特性v-ifv-show
渲染机制条件性渲染,元素创建/销毁CSS显示控制,元素始终存在
初始渲染开销低(条件为假时不渲染)高(始终渲染到DOM)
切换开销高(重新创建/销毁)低(只修改CSS)
组件生命周期触发创建/销毁钩子不触发生命周期变化
DOM结构条件为假时元素不存在元素始终在DOM中
适用场景运行时条件很少改变频繁切换显示状态

选择建议

性能优化技巧

📋 v-for列表渲染深度解析

v-for指令是Vue中最强大的功能之一,用于基于数组、对象或数字范围渲染列表。

🔬 v-for工作原理

虚拟DOM的列表diff算法

列表更新机制

  1. 依赖追踪:v-for自动追踪数组/对象的变化
  2. 虚拟DOM生成:为每个列表项创建虚拟节点
  3. diff算法:比较新旧虚拟DOM,找出最小变更
  4. DOM更新:只更新实际变化的DOM元素

key属性的作用

🔢 遍历数组的各种用法

📊 完整数组遍历示例

<template>
  <div>
    <h3>任务列表管理</h3>
    
    <!-- 基本遍历 -->
    <div class="task-list">
      <h4>任务列表(基本遍历)</h4>
      <ul>
        <li v-for="task in tasks" :key="task.id">
          <span :class="{ completed: task.completed }">{{ task.title }}</span>
          <button @click="toggleTask(task.id)">{{ task.completed ? '未完成' : '完成' }}</button>
          <button @click="removeTask(task.id)">删除</button>
        </li>
      </ul>
    </div>

    <!-- 带索引遍历 -->
    <div class="task-list">
      <h4>任务列表(带索引)</h4>
      <ul>
        <li v-for="(task, index) in tasks" :key="task.id">
          <span>{{ index + 1 }}. {{ task.title }}</span>
          <span :class="['priority', 'priority-' + task.priority]">
            优先级: {{ getPriorityText(task.priority) }}
          </span>
        </li>
      </ul>
    </div>

    <!-- 条件渲染与列表结合 -->
    <div class="task-list">
      <h4>已完成任务</h4>
      <template v-if="completedTasks.length > 0">
        <ul>
          <li v-for="task in completedTasks" :key="task.id">
            ✅ {{ task.title }}
          </li>
        </ul>
      </template>
      <p v-else>暂无已完成任务</p>
    </div>

    <!-- 统计信息 -->
    <div class="stats">
      <p>总任务数: {{ totalTasks }}</p>
      <p>已完成: {{ completedTasksCount }}</p>
      <p>未完成: {{ pendingTasksCount }}</p>
      <p>完成率: {{ completionRate }}%</p>
    </div>

    <!-- 添加新任务 -->
    <div class="add-task">
      <input v-model="newTaskTitle" placeholder="输入新任务">
      <select v-model="newTaskPriority">
        <option value="1">低优先级</option>
        <option value="2">中优先级</option>
        <option value="3">高优先级</option>
      </select>
      <button @click="addTask">添加任务</button>
    </div>
  </div>
</template>

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

// 任务数据
const tasks = ref([
  { id: 1, title: '学习Vue3基础', completed: true, priority: 3 },
  { id: 2, title: '掌握组件开发', completed: false, priority: 2 },
  { id: 3, title: '学习路由管理', completed: false, priority: 2 },
  { id: 4, title: '状态管理实践', completed: false, priority: 1 }
])

const newTaskTitle = ref('')
const newTaskPriority = ref('2')

// 计算属性
const completedTasks = computed(() => tasks.value.filter(task => task.completed))
const totalTasks = computed(() => tasks.value.length)
const completedTasksCount = computed(() => completedTasks.value.length)
const pendingTasksCount = computed(() => totalTasks.value - completedTasksCount.value)
const completionRate = computed(() => {
  return totalTasks.value > 0 ? Math.round((completedTasksCount.value / totalTasks.value) * 100) : 0
})

// 方法
function addTask() {
  if (newTaskTitle.value.trim()) {
    tasks.value.push({
      id: Date.now(),
      title: newTaskTitle.value,
      completed: false,
      priority: parseInt(newTaskPriority.value)
    })
    newTaskTitle.value = ''
  }
}

function toggleTask(id) {
  const task = tasks.value.find(t => t.id === id)
  if (task) task.completed = !task.completed
}

function removeTask(id) {
  tasks.value = tasks.value.filter(t => t.id !== id)
}

function getPriorityText(priority) {
  const priorityMap = { 1: '低', 2: '中', 3: '高' }
  return priorityMap[priority] || '未知'
}
</script>

<style scoped>
.task-list {
  margin: 20px 0;
  padding: 15px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

.completed {
  text-decoration: line-through;
  color: #888;
}

.priority { padding: 2px 8px; border-radius: 3px; color: white; }
.priority-1 { background: #28a745; }
.priority-2 { background: #ffc107; color: black; }
.priority-3 { background: #dc3545; }

.stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
  margin: 20px 0;
}

.add-task {
  display: flex;
  gap: 10px;
  align-items: center;
}
</style>

🔑 遍历对象的各种用法

👤 用户信息对象遍历示例

<template>
  <div>
    <h3>用户信息展示</h3>
    
    <!-- 基本遍历:只获取值 -->
    <div class="info-section">
      <h4>用户信息(值遍历)</h4>
      <ul>
        <li v-for="value in userInfo" :key="value">
          {{ value }}
        </li>
      </ul>
    </div>

    <!-- 带键名遍历 -->
    <div class="info-section">
      <h4>用户信息(键值对)</h4>
      <ul>
        <li v-for="(value, key) in userInfo" :key="key">
          <strong>{{ formatKey(key) }}:</strong> {{ formatValue(key, value) }}
        </li>
      </ul>
    </div>

    <!-- 带键名和索引 -->
    <div class="info-section">
      <h4>用户信息(带索引)</h4>
      <table>
        <thead>
          <tr>
            <th>序号</th>
            <th>属性名</th>
            <th>属性值</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(value, key, index) in userInfo" :key="key">
            <td>{{ index + 1 }}</td>
            <td>{{ formatKey(key) }}</td>
            <td>{{ formatValue(key, value) }}</td>
          </tr>
        </tbody>
      </table>
    </div>

    <!-- 嵌套对象遍历 -->
    <div class="info-section" v-if="userProfile">
      <h4>用户档案(嵌套对象)</h4>
      <div v-for="(section, sectionName) in userProfile" :key="sectionName">
        <h5>{{ formatSectionName(sectionName) }}</h5>
        <ul>
          <li v-for="(value, key) in section" :key="key">
            {{ formatKey(key) }}: {{ value }}
          </li>
        </ul>
      </div>
    </div>

    <!-- 编辑模式 -->
    <div class="info-section">
      <h4>编辑用户信息</h4>
      <div v-for="(value, key) in editableUserInfo" :key="key" class="edit-field">
        <label :for="key">{{ formatKey(key) }}:</label>
        <input :id="key" v-model="editableUserInfo[key]" :type="getInputType(key)">
      </div>
      <button @click="saveChanges">保存修改</button>
    </div>
  </div>
</template>

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

// 用户信息对象
const userInfo = reactive({
  name: '张三',
  age: 25,
  email: 'zhangsan@example.com',
  phone: '13800138000',
  city: '北京市',
  occupation: '前端工程师',
  joinDate: '2023-01-15'
})

// 嵌套用户档案
const userProfile = reactive({
  basic: {
    gender: '男',
    birthday: '1998-05-20',
    education: '本科'
  },
  work: {
    company: '某科技公司',
    department: '前端开发部',
    position: '高级工程师'
  },
  contact: {
    wechat: 'zhangsan123',
    qq: '123456789',
    address: '北京市朝阳区'
  }
})

// 可编辑的用户信息
const editableUserInfo = reactive({ ...userInfo })

// 格式化方法
function formatKey(key) {
  const keyMap = {
    name: '姓名', age: '年龄', email: '邮箱', phone: '电话',
    city: '城市', occupation: '职业', joinDate: '加入日期',
    gender: '性别', birthday: '生日', education: '学历',
    company: '公司', department: '部门', position: '职位',
    wechat: '微信', qq: 'QQ', address: '地址'
  }
  return keyMap[key] || key
}

function formatValue(key, value) {
  if (key === 'joinDate' || key === 'birthday') {
    return new Date(value).toLocaleDateString('zh-CN')
  }
  return value
}

function formatSectionName(sectionName) {
  const sectionMap = {
    basic: '基本信息', work: '工作信息', contact: '联系信息'
  }
  return sectionMap[sectionName] || sectionName
}

function getInputType(key) {
  if (key === 'age') return 'number'
  if (key.includes('Date') || key === 'birthday') return 'date'
  if (key === 'email') return 'email'
  if (key === 'phone') return 'tel'
  return 'text'
}

function saveChanges() {
  Object.assign(userInfo, editableUserInfo)
  alert('用户信息已更新!')
}
</script>

<style scoped>
.info-section {
  margin: 20px 0;
  padding: 15px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

table {
  width: 100%;
  border-collapse: collapse;
}

table th, table td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}

.edit-field {
  margin: 10px 0;
  display: flex;
  align-items: center;
}

.edit-field label {
  width: 100px;
  font-weight: bold;
}

.edit-field input {
  flex: 1;
  padding: 5px;
  border: 1px solid #ccc;
  border-radius: 3px;
}
</style>

🔢 遍历数字范围

<template>
  <div>
    <h3>数字范围遍历应用</h3>
    
    <!-- 基本数字遍历 -->
    <div class="number-section">
      <h4>1-10的数字</h4>
      <div class="number-grid">
        <span v-for="n in 10" :key="n" class="number-item">{{ n }}</span>
      </div>
    </div>

    <!-- 动态范围 -->
    <div class="number-section">
      <h4>自定义范围</h4>
      <div>
        <label>起始值: <input v-model.number="start" type="number"></label>
        <label>结束值: <input v-model.number="end" type="number"></label>
      </div>
      <div class="number-grid">
        <span v-for="n in range" :key="n" class="number-item">{{ n }}</span>
      </div>
    </div>

    <!-- 生成表格 -->
    <div class="number-section">
      <h4>乘法表生成器</h4>
      <table class="multiplication-table">
        <tr>
          <th>×</th>
          <th v-for="col in tableSize" :key="col">{{ col }}</th>
        </tr>
        <tr v-for="row in tableSize" :key="row">
          <th>{{ row }}</th>
          <td v-for="col in tableSize" :key="col">{{ row * col }}</td>
        </tr>
      </table>
    </div>

    <!-- 分页器 -->
    <div class="number-section">
      <h4>分页导航</h4>
      <div class="pagination">
        <button @click="prevPage" :disabled="currentPage === 1">上一页</button>
        <span v-for="page in totalPages" :key="page" 
              :class="{ active: page === currentPage }"
              @click="goToPage(page)">
          {{ page }}
        </span>
        <button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
      </div>
      <p>当前页: {{ currentPage }} / {{ totalPages }}</p>
    </div>
  </div>
</template>

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

const start = ref(1)
const end = ref(10)
const tableSize = ref(9)
const currentPage = ref(1)
const totalPages = ref(10)

// 计算属性
const range = computed(() => {
  const result = []
  for (let i = start.value; i <= end.value; i++) {
    result.push(i)
  }
  return result
})

// 方法
function prevPage() {
  if (currentPage.value > 1) currentPage.value--
}

function nextPage() {
  if (currentPage.value < totalPages.value) currentPage.value++
}

function goToPage(page) {
  currentPage.value = page
}
</script>

<style scoped>
.number-section {
  margin: 20px 0;
  padding: 15px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

.number-grid {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  gap: 5px;
  margin: 10px 0;
}

.number-item {
  padding: 8px;
  text-align: center;
  background: #f0f0f0;
  border-radius: 3px;
}

.multiplication-table {
  border-collapse: collapse;
  margin: 10px 0;
}

.multiplication-table th,
.multiplication-table td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: center;
  min-width: 40px;
}

.multiplication-table th {
  background: #f5f5f5;
  font-weight: bold;
}

.pagination {
  display: flex;
  gap: 5px;
  align-items: center;
  margin: 10px 0;
}

.pagination span {
  padding: 5px 10px;
  border: 1px solid #ddd;
  cursor: pointer;
  border-radius: 3px;
}

.pagination span.active {
  background: #42b883;
  color: white;
  border-color: #42b883;
}

.pagination button {
  padding: 5px 10px;
  border: 1px solid #ddd;
  background: white;
  cursor: pointer;
  border-radius: 3px;
}

.pagination button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
</style>

🔑 key属性的重要性深度解析

🔬 key属性工作原理

虚拟DOM的节点复用机制

没有key的情况

// 原始列表
<div>A</div>  // 索引0
<div>B</div>  // 索引1
<div>C</div>  // 索引2

// 删除第一个元素后
<div>B</div>  // 原索引1,现索引0(内容错误)
<div>C</div>  // 原索引2,现索引1(内容错误)

有key的情况

// 原始列表
<div key="1">A</div>
<div key="2">B</div>
<div key="3">C</div>

// 删除第一个元素后
<div key="2">B</div>  // 正确识别
<div key="3">C</div>  // 正确识别
原理说明:key帮助Vue识别哪些节点是相同的,从而可以复用DOM元素而不是重新创建,提高性能并保持组件状态。

⚡ key属性性能对比演示

<template>
  <div>
    <h3>key属性性能对比</h3>
    
    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
      <!-- 使用索引作为key(不推荐) -->
      <div class="demo-section">
        <h4>❌ 使用索引作为key</h4>
        <ul>
          <li v-for="(item, index) in itemsWithIndexKey" :key="index">
            <input v-model="item.text" placeholder="输入内容">
            <span>{{ item.text }}</span>
          </li>
        </ul>
        <button @click="addItemWithIndexKey">添加项目</button>
        <button @click="removeFirstItemWithIndexKey">删除第一个</button>
        <p>问题:删除第一个项目后,输入框状态会错乱</p>
      </div>

      <!-- 使用唯一ID作为key(推荐) -->
      <div class="demo-section">
        <h4>✅ 使用唯一ID作为key</h4>
        <ul>
          <li v-for="item in itemsWithIdKey" :key="item.id">
            <input v-model="item.text" placeholder="输入内容">
            <span>{{ item.text }}</span>
          </li>
        </ul>
        <button @click="addItemWithIdKey">添加项目</button>
        <button @click="removeFirstItemWithIdKey">删除第一个</button>
        <p>优势:删除项目后,其他项目的状态保持不变</p>
      </div>
    </div>

    <div class="conclusion">
      <h4>📋 结论</h4>
      <ul>
        <li><strong>使用索引作为key的问题:</strong>
          <ul>
            <li>列表重新排序时,组件状态会错乱</li>
            <li>删除/添加项目时,输入框状态可能丢失</li>
            <li>性能较差,无法充分利用虚拟DOM的复用机制</li>
          </ul>
        </li>
        <li><strong>使用唯一ID作为key的优势:</strong>
          <ul>
            <li>组件状态保持稳定</li>
            <li>性能优化,DOM复用更高效</li>
            <li>列表操作更安全可靠</li>
          </ul>
        </li>
      </ul>
    </div>
  </div>
</template>

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

// 使用索引作为key的数据
const itemsWithIndexKey = ref([
  { id: 1, text: '项目1' },
  { id: 2, text: '项目2' },
  { id: 3, text: '项目3' }
])

// 使用唯一ID作为key的数据
const itemsWithIdKey = ref([
  { id: 1, text: '项目1' },
  { id: 2, text: '项目2' },
  { id: 3, text: '项目3' }
])

function addItemWithIndexKey() {
  itemsWithIndexKey.value.push({
    id: Date.now(),
    text: '新项目'
  })
}

function removeFirstItemWithIndexKey() {
  itemsWithIndexKey.value.shift()
}

function addItemWithIdKey() {
  itemsWithIdKey.value.push({
    id: Date.now(),
    text: '新项目'
  })
}

function removeFirstItemWithIdKey() {
  itemsWithIdKey.value.shift()
}
</script>

<style scoped>
.demo-section {
  border: 2px solid #ddd;
  padding: 15px;
  border-radius: 5px;
}

.demo-section h4 {
  margin-top: 0;
}

.demo-section ul {
  list-style: none;
  padding: 0;
}

.demo-section li {
  margin: 10px 0;
  padding: 10px;
  border: 1px solid #eee;
  border-radius: 3px;
  display: flex;
  align-items: center;
  gap: 10px;
}

.demo-section input {
  padding: 5px;
  border: 1px solid #ccc;
  border-radius: 3px;
  flex: 1;
}

.conclusion {
  margin-top: 20px;
  padding: 15px;
  background: #f8f9fa;
  border-radius: 5px;
}

.conclusion ul {
  margin: 10px 0;
}

.conclusion li {
  margin: 5px 0;
}
</style>

数组变更方法

<template>
  <div>
    <ul>
      <li v-for="item in items" :key="item.id">{{ item.text }}</li>
    </ul>
    <button @click="addItem">添加</button>
    <button @click="removeItem">删除</button>
    <button @click="reverseItems">反转</button>
  </div>
</template>

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

const items = ref([
  { id: 1, text: '项目1' },
  { id: 2, text: '项目2' }
])

let nextId = 3

function addItem() {
  items.value.push({ id: nextId++, text: `项目${nextId - 1}` })
}

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

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

v-for与v-if结合

<template>
  <div>
    <!-- 不推荐:v-if和v-for在同一元素上 -->
    <li v-for="user in users" v-if="user.isActive" :key="user.id">
      {{ user.name }}
    </li>

    <!-- 推荐:使用计算属性过滤 -->
    <li v-for="user in activeUsers" :key="user.id">
      {{ user.name }}
    </li>

    <!-- 或使用template包裹 -->
    <template v-for="user in users" :key="user.id">
      <li v-if="user.isActive">{{ user.name }}</li>
    </template>
  </div>
</template>

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

const users = ref([
  { id: 1, name: '张三', isActive: true },
  { id: 2, name: '李四', isActive: false },
  { id: 3, name: '王五', isActive: true }
])

const activeUsers = computed(() => {
  return users.value.filter(user => user.isActive)
})
</script>

实战示例:待办事项列表

<template>
  <div>
    <input v-model="newTodo" @keyup.enter="addTodo" placeholder="添加待办事项">
    <button @click="addTodo">添加</button>

    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" v-model="todo.done">
        <span :style="{ textDecoration: todo.done ? 'line-through' : 'none' }">
          {{ todo.text }}
        </span>
        <button @click="removeTodo(todo.id)">删除</button>
      </li>
    </ul>

    <p v-if="todos.length === 0">暂无待办事项</p>
    <p v-else>
      已完成: {{ completedCount }} / {{ todos.length }}
    </p>
  </div>
</template>

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

const newTodo = ref('')
const todos = ref([])
let nextId = 1

function addTodo() {
  if (newTodo.value.trim()) {
    todos.value.push({
      id: nextId++,
      text: newTodo.value,
      done: false
    })
    newTodo.value = ''
  }
}

function removeTodo(id) {
  todos.value = todos.value.filter(todo => todo.id !== id)
}

const completedCount = computed(() => {
  return todos.value.filter(todo => todo.done).length
})
</script>

练习

  1. 创建一个用户列表,根据用户状态显示不同的样式
  2. 实现一个商品列表,支持添加、删除和筛选功能
  3. 创建一个表格,使用v-for渲染行和列
  4. 实现一个标签页切换功能,使用v-if或v-show