讲解

v-if 系列(v-if / v-else-if / v-else)是真正的条件渲染:条件为假时节点直接从 DOM 树移除,连组件实例都会销毁重建。与之相对,v-show 始终渲染节点,只是切换 display。取舍很直接:初次渲染成本高、切换不频繁用 v-if(比如登录/未登录两个面板);切换频繁用 v-show(比如手风琴)。v-else 必须紧跟在 v-if 之后,中间隔了别的元素就失效。

v-for 渲染列表:<li v-for="item in items" :key="item.id">。遍历对象用 (value, key) in object,还可以拿第二个参数做索引 (item, index) in items。和 v-if 一起用时注意:Vue 3 里同节点上 v-if 优先级高于 v-for,意味着 v-if 拿不到 v-for 的循环变量——想「过滤后渲染」,正确姿势是先 computed 出过滤数组,或者把 v-if 挪到内层。

key 是列表渲染的灵魂。Vue 更新列表时靠 key 识别「哪个节点是哪个」:key 稳定且唯一,Vue 就能精准复用、移动节点;没有 key 或用 index 当 key,在插入、删除、排序场景下会把节点张冠李戴——输入框内容串行、勾选状态错乱,大多是用 index 当 key 埋的雷。唯一例外是「纯静态、无状态、永不重排」的列表。

示例

用数组操作模拟「key 决定复用」的 diff 直觉,并验证过滤应该用 computed 的思路(在本教程构建时被真实执行):

import assert from 'node:assert/strict';

// 模拟:按 key 对齐新旧列表,复用未变的节点
function diffByKey(oldList, newList) {
  const oldByKey = new Map(oldList.map((n) => [n.key, n]));
  let reused = 0;
  const result = newList.map((n) => {
    const old = oldByKey.get(n.key);
    if (old) {
      reused++; // key 命中 → 复用节点,只更新数据
      return { ...n, node: old.node };
    }
    return { ...n, node: '新建节点' }; // 无 key 命中 → 新建
  });
  return { result, reused };
}

const oldList = [
  { key: 'a', node: '节点A', text: '苹果' },
  { key: 'b', node: '节点B', text: '香蕉' },
  { key: 'c', node: '节点C', text: '樱桃' },
];
// 新列表:删掉 b、a 和 c 交换顺序
const newList = [
  { key: 'c', text: '樱桃' },
  { key: 'a', text: '苹果' },
];

const { result, reused } = diffByKey(oldList, newList);
assert.strictEqual(reused, 2); // 两个节点全部复用
assert.strictEqual(result[0].node, '节点C'); // c 挪到最前,节点跟着走
assert.strictEqual(result[1].node, '节点A');

// 「先过滤再渲染」= computed 先行
const todos = [
  { id: 1, text: '写周报', done: true },
  { id: 2, text: '改 bug', done: false },
];
const remaining = todos.filter((t) => !t.done);
assert.deepStrictEqual(
  remaining.map((t) => t.text),
  ['改 bug'],
);

console.log('diff 复用节点数:', reused, ',删除 b 后没有节点被误复用');
console.log('剩余待办:', remaining.map((t) => t.text).join('、'));

模板中的完整形态:

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

const showAll = ref(false);
const todos = ref([
  { id: 1, text: '写周报', done: true },
  { id: 2, text: '改 bug', done: false },
  { id: 3, text: '评审方案', done: false },
]);
const visible = computed(() =>
  showAll.value ? todos.value : todos.value.filter((t) => !t.done),
);
</script>

<template>
  <button @click="showAll = !showAll">{{ showAll ? '只看未完成' : '显示全部' }}</button>
  <p v-if="visible.length === 0">全部完成,nice!</p>
  <ul v-else>
    <li v-for="t in visible" :key="t.id">{{ t.text }}</li>
  </ul>
</template>

常见坑

  • 用 index 当 key:只要列表会增删排序,index 就会让节点和状态错位;永远用数据的唯一 id。
  • 同节点混用 v-if 和 v-for:Vue 3 中 v-if 先执行、拿不到循环变量,要么 computed 先过滤,要么 v-if 放内层。
  • v-else 没紧跟 v-if:中间夹了注释或元素都会报编译错误。
  • key 重复:数据库里 id 撞车、用 index+index 拼接,都会让 diff 行为变得诡异,控制台有重复 key 警告一定要处理。
  • 在 v-for 里随手改原数组:遍历时 splice 会跳过元素,先拷贝或改用 filter/map 生成新数组。

小结

v-if 真销毁、v-show 只隐藏;列表渲染 key 必须唯一稳定,过滤交给 computed;「先算好数据再渲染」是模板整洁的第一原则。下一章讲样式绑定。