Code前端首页关于Code前端联系我们

Vue3中的if else怎么用?有哪些隐藏坑和实用优化技巧?

terry 1小时前 阅读数 19 #Vue

最近在做一个电商后台的新模块,团队里的实习生突然问我:“哥,Vue3的v-if/v-else是不是和Vue2一模一样?我刚才改了表单验证的条件,显示的内容却不对。”其实不止是实习生,很多刚从Vue2转过来或者初学Vue3的人,都会觉得v-if/v-else还是以前的写法,但实际上框架升级后,有不少细节调整、隐藏坑点,甚至还有专门结合Composition API的优化空间,今天就把这些知识点都串起来,从基础用法到进阶避坑、优化,一次性讲明白。

基础用法:别漏了v-else-if和必须相邻的规则

先从最简单的地方讲起,巩固一下基础,也方便后面对比,Vue3的条件渲染还是以v-if、v-else-if、v-else三个指令为主,基本逻辑和JavaScript的if-else if-else完全一致,满足哪个条件就渲染对应的 DOM 节点。

举个电商库存展示的小例子:假设商品有充足(库存>100)、紧张(0<库存<=100)、缺货(库存=0)三种状态,用Vue3写就是这样的:

<template>
  <div class="stock-status">
    <span v-if="product.stock > 100" class="green">库存充足,放心选购</span>
    <span v-else-if="product.stock > 0" class="yellow">库存紧张,仅剩{{ product.stock }}件</span>
    <span v-else class="red">暂时缺货,敬请期待</span>
  </div>
</template>
<script setup>
import { reactive } from 'vue'
const product = reactive({
  name: '无线蓝牙耳机',
  stock: 56
})
</script>

这里有个非常容易被新手忽略的点,必须相邻规则——v-else-if必须紧跟在v-if或者另一个v-else-if后面,v-else必须紧跟在v-if或者v-else-if的最后,中间不能隔任何除了注释以外的节点(包括空的div、p标签,甚至是空格换行加的隐藏文本节点?不对,空格换行不算,注释不算,其他有实际标签的都不行),刚才那个实习生犯的就是这个错:他在v-else-if前面加了一个

验证结果:
,结果库存紧张的时候,显示的还是验证结果后面跟着缺货提示,就是因为中间的分隔符把v-if和v-else-if、v-else的相邻关系打断了。

核心区别:Vue2 vs Vue3的v-if/v-for优先级变化

这是Vue3条件渲染最大的底层变化之一,也是面试里经常考的点,更是实际开发中最容易出bug的地方!在Vue2里,v-for的优先级是比v-if高的,也就是说如果同时在一个标签上用v-if和v-for,框架会先遍历所有元素,再对每个元素做条件判断;而Vue3完全反过来了,v-if的优先级现在比v-for高

这个变化不是随便改的,是官方经过大量性能测试和用户反馈后的优化——因为同时在一个标签上用v-for和v-if,本质上是一种“筛选遍历”的需求,但Vue2的写法有性能问题:哪怕90%的元素都不符合条件,也要先全部渲染出来再隐藏/删除,浪费了很多 DOM 操作和内存。

那现在Vue3里,如果我们还是直接在同一个标签上写v-if和v-for会发生什么?还是用刚才的电商例子,假设要筛选出库存充足的商品列表:

<!-- ❌ 错误写法,Vue3里这样会报错,或者显示空白(取决于筛选条件是否覆盖了for循环的初始变量) -->
<template>
  <div v-for="item in productList" v-if="item.stock > 100" :key="item.id">
    {{ item.name }}
  </div>
</template>

对,Vue3里这样直接写会直接报错“Cannot use 'v-if' on the same element as 'v-for' because 'v-if' has higher priority. This means 'v-if' will run first and the variable it uses may not exist yet.”翻译过来就是v-if先执行,但是productList.item这个变量还没被v-for遍历出来,所以找不到。

那正确的写法有哪些呢?官方给了两种推荐方案,一种是先筛选数据,再遍历(这也是最推荐的,不仅符合Vue3的优先级,性能也最好);另一种是用template标签把v-for包起来,让v-if和v-for不在同一个元素上。 先看第一种,用computed属性筛选(Composition API的写法更简洁,逻辑和视图分离):

<!-- ✅ 推荐写法1:用computed先筛选 -->
<template>
  <div v-for="item in filteredProducts" :key="item.id">
    {{ item.name }}
  </div>
</template>
<script setup>
import { reactive, computed } from 'vue'
const productList = reactive([
  { id: 1, name: '无线蓝牙耳机', stock: 200 },
  { id: 2, name: '机械键盘', stock: 45 },
  { id: 3, name: '游戏鼠标', stock: 150 }
])
// 筛选逻辑放在computed里,性能自动优化(只有productList变化时才会重新计算)
const filteredProducts = computed(() => {
  return productList.filter(item => item.stock > 100)
})
</script>

第二种是用template标签:

<!-- ✅ 写法2:用template包裹v-for,保持相邻性的同时分开优先级 -->
<template>
  <template v-for="item in productList" :key="item.id">
    <div v-if="item.stock > 100">
      {{ item.name }}
    </div>
  </template>
</template>

这里要注意两点:第一,template标签本身不会渲染成真实的 DOM 节点,所以不会影响页面结构;第二,v-for的key必须放在template标签上,不能放在里面的div上,因为现在v-for是绑定在template上的。

隐藏坑点:除了优先级,还有这三个容易踩的雷

刚才说了实习生踩的相邻规则的坑,还有优先级的坑,其实还有三个不太明显但很容易出问题的地方,我自己或者身边的同事都踩过。

坑点1:v-if切换时的元素复用机制(key的重要性)

很多人可能不知道,Vue默认会复用相同类型的 DOM 节点来提高性能,比如v-if和v-else渲染的都是input标签,那Vue会直接复用这个input的 DOM 结构,只更新里面的内容,而不会重新创建一个,听起来是好事,但有时候会出问题。

举个登录/注册表单切换的例子:

<!-- ❌ 有bug的写法 -->
<template>
  <div>
    <button @click="isLogin = !isLogin">切换到{{ isLogin ? '注册' : '登录' }}</button>
    <form>
      <input v-if="isLogin" type="text" placeholder="请输入用户名" />
      <input v-else type="email" placeholder="请输入邮箱" />
      <input type="password" placeholder="请输入密码" />
    </form>
  </div>
</template>
<script setup>
import { ref } from 'vue'
const isLogin = ref(true)
</script>

你可以试一下:先在登录表单的用户名框里输入“张三”,然后点击切换到注册,你会发现邮箱框里居然还留着“张三”!这就是元素复用导致的——因为两个input都是input类型,Vue没区分它们,直接复用了DOM,连用户输入的内容都带过来了。

怎么解决?很简单,给这两个需要区分的input标签加上不同的key:

<!-- ✅ 加key区分元素,避免复用 -->
<template>
  <div>
    <button @click="isLogin = !isLogin">切换到{{ isLogin ? '注册' : '登录' }}</button>
    <form>
      <input v-if="isLogin" key="login-username" type="text" placeholder="请输入用户名" />
      <input v-else key="register-email" type="email" placeholder="请输入邮箱" />
      <input type="password" placeholder="请输入密码" />
    </form>
  </div>
</template>

加了不同的key之后,Vue就知道这是两个完全不同的元素,切换时会先销毁旧的,再创建新的,用户输入的内容自然就不会留着了。

坑点2:v-else绑定的是最近的v-if/v-else-if,哪怕中间隔了动态组件?

刚才说过相邻规则,但这里有个特殊情况——如果中间隔的是这种动态组件,会不会影响v-else的绑定?答案是会的!因为动态组件也是一个实际的标签(虽然渲染的内容是动态的),所以会打断相邻关系。

再举个小例子:

<!-- ❌ 错误绑定,v-else绑定的是中间的动态组件?不,动态组件没有v-if,所以v-else找不到对应的条件,会直接不渲染或者报错(取决于版本) -->
<template>
  <div>
    <span v-if="isShowA">组件A</span>
    <component :is="isShowB ? 'component-b' : 'component-c'" />
    <span v-else>组件A以外的内容</span>
  </div>
</template>

这时候v-else就找不到最近的带v-if或v-else-if的元素了,因为中间隔了,所以要么报错,要么v-else根本不生效,解决方法还是一样,要么调整结构让v-if和v-else相邻,要么把中间的内容也放进v-if的分支里,或者用v-if/v-else-if把所有条件都覆盖。

坑点3:v-if在组件根元素上时的透传问题

在Vue3里,组件可以有多个根元素了(这也是一个大升级),但如果组件只有一个根元素,而且这个根元素上用了v-if,那父组件透传的属性(比如class、style、attrs)会透传到哪里呢?

假设我们有一个Button组件:

<!-- Button.vue组件,只有一个根元素,用了v-if -->
<template>
  <button v-if="isVisible" class="base-btn">点击我</button>
</template>
<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>

然后在父组件里用它,透传一个class:

<!-- 父组件 -->
<template>
  <div>
    <Button class="custom-btn" />
  </div>
</template>

这时候如果isVisible是true,透传的custom-btn会正常加到button的class上;但如果isVisible是false,组件的根元素就不存在了,那透传的属性就会“丢失”——不过这个丢失不是真的丢了,而是挂在了组件的实例上,但不会渲染到页面上,如果想要透传属性始终有效,要么给组件加一个不会被销毁的根元素(比如一个空的div,把v-if移到里面的button上),要么处理attrs的透传逻辑。

加一个不会被销毁的根元素是最简单的方法:

<!-- ✅ Button.vue组件,加一个固定的根div -->
<template>
  <div>
    <button v-if="isVisible" class="base-btn">点击我</button>
  </div>
</template>

这样不管isVisible是true还是false,父组件透传的class、style都会加到这个固定的根div上,不会丢失。

实用优化技巧:结合Composition API和新语法,提升开发效率和性能

刚才讲了基础用法和避坑,现在讲几个进阶的优化技巧,都是在实际开发中能用得上的。

技巧1:用computed替代复杂的内联条件

有时候条件判断会很复杂,比如有多个变量、多个逻辑与或非的组合,这时候把条件写在template的v-if里会很臃肿,可读性很差,也不利于维护和测试,这时候就可以用computed属性把复杂的条件抽出来,放在script里。

比如一个用户权限判断的需求:只有管理员、且已登录、且账号未过期的用户,才能看到后台管理的入口:

<!-- ❌ 内联条件太复杂,可读性差 -->
<template>
  <div v-if="user.isLoggedIn && user.role === 'admin' && user.expireTime > Date.now()">
    后台管理入口
  </div>
</template>

用computed抽出来之后:

<!-- ✅ 用computed抽离复杂条件,可读性好,便于维护 -->
<template>
  <div v-if="canAccessAdmin">
    后台管理入口
  </div>
</template>
<script setup>
import { reactive, computed } from 'vue'
const user = reactive({
  isLoggedIn: true,
  role: 'admin',
  expireTime: Date.now() + 86400000 // 明天过期
})
const canAccessAdmin = computed(() => {
  return user.isLoggedIn && user.role === 'admin' && user.expireTime > Date.now()
})
</script>

这样不仅template里的代码干净了,computed属性还有自动缓存的功能——只有当user的isLoggedIn、role、expireTime这三个变量变化时,canAccessAdmin才会重新计算,否则直接用上次的结果,性能也比内联条件好(内联条件每次组件渲染都会重新执行)。

技巧2:用v-show替代频繁切换的v-if

v-if和v-show的区别应该很多人都知道,但还是要再强调一下适用场景:v-if是“真正的条件渲染”,满足条件时才会创建 DOM 节点,不满足时直接销毁;v-show是“伪条件渲染”,不管满足不满足条件,都会创建 DOM 节点,只是通过CSS的display属性来控制显示和隐藏。

那什么时候用v-if,什么时候用v-show呢?切换频率很低,或者内容很多、创建销毁成本高,用v-if;如果内容切换频率很高(比如点击按钮切换一个弹窗、一个标签页的内容),用v-show

举个标签页切换的例子:假设标签页有“商品列表”、“订单列表”、“用户列表”三个,用户会频繁切换,这时候用v-show比v-if好:

<!-- ✅ 频繁切换用v-show -->
<template>
  <div>
    <div class="tabs">
      <button @click="activeTab = 'products'" :class="{ active: activeTab === 'products' }">商品列表</button>
      <button @click="activeTab = 'orders'" :class="{ active: activeTab === 'orders' }">订单列表</button>
      <button @click="activeTab = 'users'" :class="{ active: activeTab === 'users' }">用户列表</button>
    </div>
    <div v-show="activeTab === 'products'">商品列表内容</div>
    <div v-show="activeTab === 'orders'">订单列表内容</div>
    <div v-show="activeTab === 'users'">用户列表内容</div>
  </div>
</template>
<script setup>
import { ref } from 'vue'
const activeTab = ref('products')
</script>

如果用v-if的话,每次切换标签页都要销毁旧的 DOM 节点,创建新的,会有轻微的卡顿(尤其是内容很多的时候);用v-show的话,DOM 节点一直都在,只是切换display属性,非常快。

技巧3:用动态组件 + v-if/v-else实现更灵活的多状态渲染

刚才的标签页切换例子,用v-show可以,但如果每个标签页的内容是不同的组件,而且组件比较大,创建销毁成本高但切换频率不是特别高(比如后台的不同功能模块),这时候可以用动态组件 + v-if/v-else来实现更灵活的渲染。

举个例子:

<!-- ✅ 动态组件 + v-if/v-else实现灵活的多状态渲染 -->
<template>
  <div>
    <div class="tabs">
      <button @click="activeModule = 'ProductModule'" :class="{ active: activeModule === 'ProductModule' }">商品管理</button>
      <button @click="activeModule = 'OrderModule'" :class="{ active: activeModule === 'OrderModule' }">订单管理</button>
      <button @click="activeModule = 'UserModule'" :class="{ active: activeModule === 'UserModule' }">用户管理</button>
    </div>
    <!-- 只有活跃的模块才会被渲染,不活跃的直接销毁,节省内存 -->
    <ProductModule v-if="activeModule === 'ProductModule'" />
    <OrderModule v-else-if="activeModule === 'OrderModule'" />
    <UserModule v-else />
  </div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue'
// 用defineAsyncComponent异步加载组件,进一步提升首屏加载速度
const ProductModule = defineAsyncComponent(() => import('./ProductModule.vue'))
const OrderModule = defineAsyncComponent(() => import('./OrderModule.vue'))
const UserModule = defineAsyncComponent(() => import('./UserModule.vue'))
const activeModule = ref('ProductModule')
</script>

这里还用到了defineAsyncComponent异步加载组件,首屏加载的时候只会加载ProductModule,用户点击其他标签页的时候才会加载对应的组件,进一步提升了首屏加载速度。

今天我们从基础用法、核心区别(优先级变化)、隐藏坑点(相邻规则、元素复用、组件根元素透传)、实用优化技巧(computed抽离条件、v-show替代频繁切换的v-if、动态组件+异步加载)四个方面,详细讲了Vue3中的if else(也就是v-if/v-else-if/v-else)的所有重要知识点。

总结一下重点:

  1. 基础用法要遵守必须相邻规则;
  2. Vue3里v-if的优先级比v-for高,不能直接在同一个标签上用,推荐用computed先筛选;
  3. 要注意元素复用机制,给需要区分的元素加不同的key;
  4. 频繁切换用v-show,不频繁切换或内容多用v-if;
  5. 复杂条件用computed抽离,多组件大模块用动态组件+异步加载。

希望这篇文章能帮到正在学习Vue3或者刚从Vue2转过来的你,如果有什么问题,欢迎在评论区留言讨论。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门