Skip to content

05|条件渲染:该拆台时拆台,该拉帘时拉帘 ​

第一章概览 / 05。目标:按切换成本、初始成本和状态生命周期选择 v-if 或 v-show,而不是拿它们当同义词。

第一个例子是可独立运行的 src/App.vue,环境为 Vue 3.5.x + Vite + TypeScript。切换状态时同时看页面与 Elements 面板:节点消失和节点带着 display: none 留在 DOM,是两条不同证据。

一句话决策表 ​

问题倾向选择原因
条件大多数时候为假、出现时才需创建v-if未满足时不创建子树,也不会运行其中组件生命周期
内容已存在、短时间高频显示/隐藏v-show元素保留在 DOM,只切换 CSS display
需要保存输入框/组件内部状态v-show 或把状态上提v-if 切走后会卸载并重新创建
需要按条件延迟加载昂贵组件v-if(配合异步组件)先避免创建,再谈网络与代码分割

基础分支:让状态名称说人话 ​

vue
<!-- src/App.vue -->
<script setup lang="ts">
import { computed, ref } from 'vue'

type LoadState = 'idle' | 'loading' | 'success' | 'error'

const state = ref<LoadState>('idle')
const error = ref<Error | null>(null)
const hasUnreadTips = computed(() => state.value === 'success')

function retry() {
  error.value = null
  state.value = 'loading'
}
</script>

<template>
  <section aria-live="polite">
    <p v-if="state === 'idle'">还没开始,咖啡也还没凉。</p>
    <p v-else-if="state === 'loading'">正在加载…</p>
    <p v-else-if="state === 'error'" role="alert">
      {{ error?.message ?? '加载失败,请重试。' }}
      <button @click="retry">重试</button>
    </p>
    <p v-else>数据已就绪,仪表盘可以渲染。</p>
  </section>

  <!-- 已挂载的提示面板,可频繁开关。 -->
  <aside v-show="hasUnreadTips">你有新的学习提示。</aside>
</template>

v-else-if / v-else 必须紧挨着对应的 v-if / v-else-if;中间插一个别的元素,Vue 就会失去剧情接续能力。需要包裹多项条件内容时,用 <template v-if="…">,它本身不会额外生成 DOM。

生命周期差异:不是“看不见”那么简单 ​

vue
<script setup lang="ts">
import { ref } from 'vue'
import SearchPanel from './SearchPanel.vue'

const useLazyPanel = ref(true)
const visible = ref(false)
</script>

<template>
  <button @click="visible = !visible">切换面板</button>

  <!-- 每次 visible 从 false 变 true 都会创建;切回 false 会卸载。 -->
  <SearchPanel v-if="useLazyPanel && visible" />

  <!-- 首次渲染时即创建;之后只是 display:none / 显示。 -->
  <SearchPanel v-show="!useLazyPanel && visible" />
</template>
维度v-ifv-show
首次条件为假不创建内容创建内容但隐藏
切换挂载 / 卸载改 display
组件内部状态卸载后丢失(除非外部保存)留在内存与 DOM 中
适用稀少、昂贵、需要真正离场高频切换、保留输入/焦点状态
限制切换有创建成本不支持 <template>,也不能和 v-else 链搭配

深水区:先画状态生命周期,再选指令 ​

条件渲染最容易犯的错,是先问“哪条指令更快”,再补理由。更可靠的顺序是先写出这块 UI 的生命周期:它何时第一次出现、关掉后是否该保留草稿、后台工作是否应继续、下次打开是恢复现场还是重新开始。答案自然会指向 v-if 或 v-show。

场景关闭后该发生什么更自然的选择
首次使用引导彻底结束,不再保留内部步骤v-if,或由上层状态决定是否再创建
常用筛选面板保留输入和滚动位置,快速再次打开v-show
危险操作确认框每次打开都是一段新流程v-if,并在关闭时让内部状态卸载
隐藏但仍需后台持续更新的监控面板DOM 与组件继续存在v-show,同时明确资源开销

v-show 隐藏的组件仍在运行它自己的定时器、watcher 和请求逻辑;它不是暂停按钮。v-if 卸载组件也不等于所有外部副作用自动消失——例如你手写的全局事件监听器仍应在组件清理阶段被正确移除。指令决定的是组件树的存在与显示,资源管理仍是代码的责任。

条件渲染最适合表达“状态”,不是为了省标签 ​

加载、空数据、权限不足、失败和成功是不同的产品状态,应该有可辨认的名称和可访问的呈现。把它们揉成 items.length ? … : … 的多层三元表达式,页面一开始很短,后来却很难补重试、说明和焦点策略。

vue
<script setup lang="ts">
import { computed, ref } from 'vue'

type ResultState = 'loading' | 'error' | 'ready'
const state = ref<ResultState>('loading')
const lessons = ref<string[]>([])
const hasLessons = computed(() => lessons.value.length > 0)
</script>

<template>
  <section aria-live="polite">
    <p v-if="state === 'loading'">正在读取课程…</p>
    <p v-else-if="state === 'error'" role="alert">加载失败,请重试。</p>
    <p v-else-if="!hasLessons">还没有课程,先创建第一条吧。</p>
    <ul v-else>
      <li v-for="lesson in lessons" :key="lesson">{{ lesson }}</li>
    </ul>
  </section>
</template>

这里的 v-if 链回答的是“用户现在处于什么阶段”;列表中的 v-for 只回答“该阶段要展示哪些项”。把两个问题拆开,才能避免又在同一元素上混用 v-if 和 v-for。

分支身份与 key ​

Vue 3会为 v-if/v-else 分支自动生成不同key,因此下面的邮箱与手机分支即使删掉手工key,切换时也会重建。若自己写key,必须保证各分支不同;不能通过相同key强迫Vue3复用分支。这个规则与后文同一 v-for 列表按key匹配条目不同。

vue
<script setup lang="ts">
import { ref } from 'vue'
const mode = ref<'email' | 'phone'>('email')
</script>

<template>
  <label v-if="mode === 'email'" key="email-field">
    邮箱 <input type="email" autocomplete="email" />
  </label>
  <label v-else key="phone-field">
    手机号 <input type="tel" autocomplete="tel" />
  </label>
</template>

容易混淆的交叉口:不要把 v-if 和 v-for 写在同一元素 ​

它们同时出现时 v-if 的优先级更高,表达式还拿不到 v-for 的局部变量。更重要的是,把过滤逻辑写进模板会让身份、性能和可读性一起挠头。先用计算属性生成展示列表,再只渲染它。

vue
<script setup lang="ts">
import { computed, ref } from 'vue'

type Task = { id: string; title: string; done: boolean }
const tasks = ref<Task[]>([])
const activeTasks = computed(() => tasks.value.filter((task) => !task.done))
</script>

<template>
  <!-- 好:过滤是派生数据,渲染只负责逐项展示。 -->
  <li v-for="task in activeTasks" :key="task.id">{{ task.title }}</li>
</template>

排错路线:切换后数据去哪儿了 ​

  1. 确认条件值。 条件表达式是否真的是布尔语义?加载状态、权限状态与“用户手动关闭”最好不要混在一个含糊的变量里。
  2. 确认节点是否仍存在。 在 Elements 面板中,v-if 关闭后节点会消失,v-show 仍在但带有隐藏样式;这决定了 ref、焦点与内部状态的命运。
  3. 检查分支身份。 Vue3条件分支已自动具有不同身份;若要保留数据,保存的是业务状态,而不是试图让不同分支共用key。
  4. 把列表逻辑带离模板。 当条件涉及筛选一组项时,先用 computed 生成集合,再使用单纯的 v-for 渲染。

翻车博物馆 ​

现象真相修复
展开高级筛选后输入全没了用了 v-if,组件已经卸载用 v-show,或把草稿状态提升到父层
隐藏面板仍在跑计时器/请求v-show 只是隐藏需要停止工作时用 v-if 或由组件显式暂停
v-else 不生效与 v-if 不是相邻兄弟调整结构,必要时用 <template> 包组
列表筛选写成 v-for + v-if优先级与维护性都糟糕计算属性先筛选

练习:给设置页选对开关 ​

创建 src/App.vue、src/components/HelpDrawer.vue 和 src/components/DeleteConfirm.vue。实验功能初始关闭,第一次打开才创建;帮助抽屉内有一个本地搜索草稿,关闭再打开必须保留;危险确认框内要求输入 DELETE,取消再打开必须清空。

为三块分别选择 v-if 或 v-show,并写下“初始是否创建、关闭后局部状态是否保留、内部资源是否继续运行”的推导。不要把局部草稿都放到 App.vue,否则无法观察卸载与隐藏的真实差异。

参考答案:查看第一章练习答案中的第 05 题。

验收清单:如何判断练习完成 ​

  • 首屏 Elements 中没有实验功能与危险确认框;帮助抽屉节点存在但隐藏。
  • 帮助抽屉输入 key 后关闭再打开仍保留;确认框输入 DELETE 后取消再打开为空。
  • 每个 v-if / v-show 选择都能用创建成本、切换频率、状态生命周期和资源是否继续运行来解释。
  • 列表过滤先在 computed 中完成,没有把 v-if 和 v-for 放在同一元素上赌优先级。

通关自检:你能用“创建成本、切换频率、状态生命周期”三个词解释选择,而不是只说“这两个都能隐藏”。

用心记录每一步成长