Vue条件渲染中v-if与v-show如何抉择?重复渲染问题又该如何优化?

38 阅读18分钟

条件渲染的基本逻辑:v-if vs v-show

先从最基础的说起——Vue里最常用的两个条件渲染指令:v-ifv-show。很多初学者刚接触时会混淆它们,其实核心区别就一个:是否真正销毁DOM节点

v-if是“惰性选手”:只有当条件为真时,才会创建DOM节点;条件为假时,直接把节点从DOM树里删掉。比如一个管理员才能看到的按钮,用v-if="isAdmin",如果用户不是管理员,这个按钮根本不会出现在DOM里。

v-show是“隐藏选手”:不管条件真假,先把DOM节点渲染出来,然后用CSS的display: none来切换显隐。比如一个频繁切换的tab栏,用v-show就比v-if更划算——因为切换时只是改个CSS属性,不用反复创建/销毁节点。

举个直观的例子:假设你有个“夜间模式”开关,需要频繁点按。用v-show的话,第一次渲染时就把夜间模式的样式容器建好,切换时只需要把displaynone改成block;而用v-if的话,每次切换都要重新创建/销毁这个容器,相当于反复拆建房子,肯定更费性能。

为什么会出现“重复渲染”?

在讲优化之前,得先搞明白:为什么条件渲染会导致重复渲染?

Vue的响应式系统是“依赖收集+触发更新”的模式:当你用refreactive定义数据时,Vue会跟踪每个数据被哪些组件/表达式使用(依赖收集);当数据变化时,Vue会通知所有依赖它的部分重新渲染(触发更新)。

如果你的条件表达式里藏了很多“不必要的依赖”,或者条件切换时触发了过多的节点重建,就会导致重复渲染——比如:

  1. 条件表达式太复杂:比如v-if="a && b || c && !d",其中abcd都是响应式数据,任何一个变化都会触发条件重新计算,进而导致组件更新。
  2. 子组件被“连坐”:父组件的条件变化时,即使子组件的props没改,子组件也会跟着重新渲染(因为父组件更新会触发所有子组件的更新)。
  3. 节点复用导致的“状态残留”:Vue为了性能会复用相同结构的节点,比如两个v-if分支里的输入框,如果不用key区分,切换时输入框的内容会保留,看似“省性能”,但实际可能导致逻辑错误(比如登录和注册表单的输入框复用)。

优化技巧1:用key给节点“贴身份证”

key是Vue里最被低估的优化工具之一——它相当于节点的“唯一标识”,Vue通过key判断两个节点是不是同一个,从而决定是复用还是重建。

场景1:条件分支的“状态隔离”

比如你有个切换登录/注册的组件:

<div v-if="isLogin">
  <input placeholder="用户名" />
</div>
<div v-else>
  <input placeholder="邮箱" />
</div>

如果没有key,切换isLogin时,Vue会复用输入框节点—— placeholder变了,但输入的内容会保留(比如你在登录框输入了“张三”,切换到注册框,输入框里还是“张三”)。这显然不符合预期。

解决办法很简单:给两个分支加不同的key

<div v-if="isLogin" key="login">
  <input placeholder="用户名" />
</div>
<div v-else key="register">
  <input placeholder="邮箱" />
</div>

key变了,Vue就会认为这是两个完全不同的节点,切换时会销毁旧节点、创建新节点,输入框的内容自然就清空了。

场景2:列表渲染的“性能保障”

列表渲染里的key更重要。比如你有个 todo 列表:

<!-- 错误:用索引当key -->
<li v-for="(item, index) in todos" :key="index">
  {{ item.text }}
</li>

如果删除中间的一个todo,后面的所有index都会变(比如第3个变成第2个),Vue会认为这些节点都“变了”,从而重新渲染所有后续节点——如果列表很长,这会非常卡。

正确的做法是用唯一标识key(比如后端返回的id):

<li v-for="item in todos" :key="item.id">
  {{ item.text }}
</li>

这样即使列表变化,Vue也能准确识别哪些节点需要保留、哪些需要更新,避免不必要的重新渲染。

优化技巧2:选对v-ifv-show,别“用错工具”

很多人问:“到底什么时候用v-if,什么时候用v-show?”其实判断标准就一个:切换频率

  • 频繁切换(比如tab、弹窗):用v-show。虽然初始化时要渲染节点,但切换成本低(改CSS)。
  • 不频繁切换(比如权限控制、页面初始化):用v-if。虽然切换成本高,但初始化时不渲染节点,节省DOM资源。

举个例子:你的页面有个“反馈弹窗”,用户可能频繁点击开关——用v-show;而“管理员设置”页面,只有管理员登录才会显示——用v-if

优化技巧3:用计算属性“简化条件”

如果你的条件表达式越来越复杂(比如v-if="a && b || (c && !d) && e"),千万别直接写在模板里!因为模板里的表达式会在每次组件更新时重新计算,即使依赖的数据没变化。

正确的做法是把复杂条件抽到计算属性里——计算属性会缓存结果,只有依赖的数据变化时才会重新计算。

比如你有个商品列表,要显示“折扣且有库存”的商品:

<!-- 错误:模板里写复杂条件 -->
<div v-for="item in items" v-if="item.discount > 0 && item.stock > 0" :key="item.id">
  {{ item.name }}
</div>

改成计算属性:

<template>
  <div v-for="item in discountedItems" :key="item.id">
    {{ item.name }}
  </div>
</template>

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

const items = ref([
  { id: 1, name: '手机', discount: 0.8, stock: 10 },
  { id: 2, name: '电脑', discount: 0, stock: 5 },
  { id: 3, name: '平板', discount: 0.7, stock: 0 }
])

// 计算属性:过滤折扣且有库存的商品
const discountedItems = computed(() => {
  return items.value.filter(item => item.discount > 0 && item.stock > 0)
})
</script>

这样一来,只有items里的discountstock变化时,discountedItems才会重新计算,比模板里的条件表达式高效得多。

优化技巧4:用Teleport隔离“独立组件”

有没有遇到过这种情况?你的模态框放在父组件里,父组件的条件变化时,模态框也跟着重新渲染?比如父组件用v-if控制一个列表,每次列表更新,模态框都要重新创建——这其实是没必要的,因为模态框的逻辑和父组件无关。

这时候Teleport就派上用场了——它能把组件的DOM节点“传送”到其他位置(比如body),从而脱离父组件的DOM树,避免父组件的更新影响它。

比如一个模态框:

<template>
  <button @click="showModal = true">打开模态框</button>

  <!-- 用Teleport把模态框传送到body下 -->
  <Teleport to="body">
    <div v-if="showModal" class="modal">
      <div class="modal-content">
        <h3>我是模态框</h3>
        <button @click="showModal = false">关闭</button>
      </div>
    </div>
  </Teleport>
</template>

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

<style scoped>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}
.modal-content {
  background: white;
  padding: 20px;
  border-radius: 8px;
}
</style>

Teleport把模态框的DOM节点放到了body下,父组件的任何更新都不会影响它——因为它已经“跳出”了父组件的DOM树。

往期文章归档
免费好用的热门在线工具

优化技巧5:拆分组件+KeepAlive,缓存“频繁切换的组件”

如果你的条件渲染涉及频繁切换的复杂组件(比如tab栏里的表单),反复创建/销毁组件会很耗性能——这时候KeepAlive组件能帮你“缓存”组件实例,避免重复渲染。

比如一个切换登录/注册的tab栏:

<template>
  <div class="tab-bar">
    <button @click="activeTab = 'login'" :class="{ active: activeTab === 'login' }">登录</button>
    <button @click="activeTab = 'register'" :class="{ active: activeTab === 'register' }">注册</button>
  </div>

  <!-- 用KeepAlive缓存组件 -->
  <KeepAlive>
    <component :is="activeTab === 'login' ? LoginForm : RegisterForm" :key="activeTab" />
  </KeepAlive>
</template>

<script setup>
import { ref } from 'vue'
import LoginForm from './LoginForm.vue'
import RegisterForm from './RegisterForm.vue'

const activeTab = ref('login')
</script>

<style scoped>
.tab-bar {
  margin-bottom: 20px;
}
.tab-bar button {
  padding: 8px 16px;
  margin-right: 10px;
  cursor: pointer;
}
.tab-bar button.active {
  background: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
}
</style>

KeepAlive会把LoginFormRegisterForm的实例缓存起来,切换tab时不会重新创建——输入框的内容会保留,性能也提升了。

优化技巧6:避免“不必要的响应式依赖”

有时候,条件渲染的重复渲染是因为条件表达式里包含了太多响应式数据。比如:

<div v-if="user && user.address && user.address.city === 'Beijing'">
  你在北京!
</div>

这里user是一个reactive对象,任何user的属性变化(比如user.name)都会触发条件重新计算——即使city没改。

解决办法有两个:

  1. 把需要的属性单独抽成ref
    import { ref, computed } from 'vue'
    
    const user = ref({
      name: '张三',
      address: { city: 'Beijing' }
    })
    
    // 把city单独抽成ref
    const city = ref(user.value.address.city)
    
    // 条件用city的值
    const isBeijing = computed(() => city.value === 'Beijing')
    
  2. toRaw获取原始对象
    import { ref, toRaw, computed } from 'vue'
    
    const user = ref({
      name: '张三',
      address: { city: 'Beijing' }
    })
    
    // 获取原始对象(非响应式)
    const rawUser = toRaw(user.value)
    
    // 条件用原始对象的属性
    const isBeijing = computed(() => rawUser.address.city === 'Beijing')
    

这样一来,只有city变化时,条件才会重新计算,避免了不必要的更新。

课后Quiz:巩固一下

  1. 问题v-ifv-show的核心区别是什么?分别适合什么场景? 答案v-if是“销毁/创建DOM节点”,适合不频繁切换的场景(如权限控制);v-show是“CSS显隐”,适合频繁切换的场景(如tab栏)。 解析:参考Vue官网的条件渲染文档:vuejs.org/guide/essen…

  2. 问题:为什么列表渲染时不能用索引当key答案:索引是动态的,当列表添加/删除/排序时,索引会变化,导致Vue认为是不同的节点,从而重新渲染所有后续节点,影响性能。 解析:参考Vue官网的列表渲染文档:vuejs.org/guide/essen…

  3. 问题KeepAlive组件的作用是什么?请写出一个使用KeepAlive的示例。 答案KeepAlive用于缓存组件实例,避免频繁创建/销毁。示例:

    <KeepAlive>
      <component :is="activeComponent" :key="activeComponent" />
    </KeepAlive>
    

    解析:参考Vue官网的KeepAlive文档:vuejs.org/guide/built…

常见报错解决方案

报错1:Duplicate keys detected: 'xxx'. This may cause an update error.

  • 原因:多个节点用了相同的key,Vue无法识别唯一节点。
  • 解决:确保每个节点的key唯一,比如用数据的id代替索引。
  • 预防:列表渲染时优先用唯一标识当key,条件分支用不同的key

报错2:v-if and v-for on the same element is not recommended.

  • 原因v-for优先级比v-if高,导致每次循环都要判断条件,性能差。
  • 解决:用计算属性过滤列表,再用v-for循环过滤后的结果。
  • 预防:避免在同一个元素上同时用v-forv-if

报错3:Component is missing template or render function.

  • 原因:组件没有定义模板或渲染函数,比如忘记写<template>标签。
  • 解决:检查组件是否有<template>标签(单文件组件)或render函数(函数式组件)。
  • 预防:创建组件时确保有模板或渲染函数。

参考链接