ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Vue3条件渲染核心解析:v-if/v-show、效率与最佳实践

2026/9/9 16:25:28 拓冰建站 浏览量
Vue3条件渲染核心解析:v-if/v-show、效率与最佳实践 1. 条件渲染的核心思路先理解条件语句到底在解决什么问题1.1 条件语句在Vue3项目里的真实位置不管是Vue2还是Vue3只要你在写前端页面基本都逃不过条件渲染这件事。Vue3里最常用的就是v-if、v-else-if、v-else和v-show这四个指令。很多人觉得这个太基础了没什么好聊的但实际去翻项目代码你会发现条件语句用得好不好直接决定了组件代码的可读性和运行性能。我之前帮朋友排查过一个后台管理系统的卡顿问题页面每次切换菜单都要卡几百毫秒最后定位到问题出在一个列表页里同时用了十几个v-if嵌套而且每个分支里都直接调用方法来过滤数据。模板里到处是v-iflist.filter(x x.type a).length 0这种写法数据一多就吃不消。所以说条件语句虽然入门简单但真要写出高效、好维护的代码还是得把原理和取舍搞清楚。Vue3条件语句的本质就是根据响应式数据的状态决定一段DOM节点是否渲染、以什么方式渲染。v-if是真正的条件渲染它会根据表达式的真假决定是否创建或销毁DOM元素v-show则不管真假都会渲染只是通过display属性来控制显隐。这个区别是所有后续选择的根基。1.2 v-if和v-show的取舍我见过太多人用错很多新手上来就直接用v-if也没想过为什么。实际上两者最核心的差异就一句话v-if是要不要的问题v-show是看不看的问题。v-if在条件为false时对应的DOM节点根本不会出现在页面里组件也会被销毁条件为true时Vue会重新创建组件实例、执行生命周期钩子再进行挂载。这个创建和销毁的过程是有成本的尤其是组件层级比较深、内部状态复杂的时候。而v-show不管条件真假DOM节点始终存在只是切换display: none和display: block所以切换成本很低不涉及生命周期。那怎么选我的经验是如果某个模块需要根据状态频繁切换显示隐藏比如Tab切换、折叠面板、弹窗内部的面板切换优先用v-show。你不想每次切换都让组件重新初始化一遍否则里面的表单输入、滚动位置、内部定时器全部重置体验会很差。如果某个模块只在特定条件下才需要渲染而且条件很少变化比如用户有权限才能看到某个按钮、某个模块只在新手引导阶段出现优先用v-if。这样可以减少初始渲染的DOM数量还能避免那些没必要加载的组件被创建。还有一个细节容易被忽略v-show不支持在template上使用。原因很简单template本身不会渲染成真实的DOM节点它只是个逻辑容器v-show无法给它挂上display样式。如果非要给一组元素做显隐切换可以用一个包裹的div或者直接用v-if配合template。1.3 v-else-if和v-else的组合逻辑别写成一坨面条v-else-if和v-else必须紧跟在v-if或另一个v-else-if后面中间不能插其他元素否则Vue会直接报错或者在控制台提示无法识别。这个规则没什么难度但实际项目里最容易出现的问题是分支太多、嵌套太深模板文件读起来像一碗面条。我见过最夸张的一个代码一个单元格里写了七八个v-if链每个分支里还有两三层嵌套的条件业务逻辑全部堆在模板里。这种代码不是不能跑而是维护成本极高。过两个月你去看根本分不清哪个分支对应哪个状态。解决嵌套过深的问题我的建议是分两层处理。第一层用computed把原始状态映射成统一的类型值比如把status 1 || status 2这种复杂判断收敛成statusText success第二层模板里再用v-ifstatusText success去做分支。这样模板会干净很多逻辑也放在JS里容易单测。2. 模板语法里的实操细节从入门到不踩坑2.1 template空包裹条件分支里最容易被忽略的容器如果你需要同时控制多个相邻元素的条件渲染template是一个非常好用的容器。它不会渲染成真实的DOM节点所以不会影响布局和样式结构。举个例子你有一个卡片组件需要同时展示标题和描述这两个元素要么都显示要么都不显示但中间不能包一层div因为样式结构已经定死了。这时候就可以写template v-ifshowCard h2{{ title }}/h2 p{{ description }}/p /template这段代码在最终渲染结果里只会有h2和p不会多出任何包裹节点。在Vue3的Fragment机制下组件本身也可以返回多个根节点template的使用场景其实更多了。比如在一些组件封装里我想让外部使用者通过插槽传入多个节点内部用v-if控制是否渲染用template就很方便。2.2 v-if和v-for同时出现Vue3的顺序已经变了这个知识点在面试里被问的概率极高而且很多人还在用Vue2的习惯写Vue3代码。Vue2中v-for的优先级高于v-if也就是说v-if会作用在每一个循环出来的元素上判断能否渲染。这意味着即使你的v-if条件整体为falseVue还是会先遍历整个列表白白浪费性能。Vue3的改变是v-if的优先级高于v-for。如果v-if的条件为false整个循环根本不会执行。看起来性能更好了但这会引入一个新的问题在v-if里你拿不到v-for作用域里的变量。我实际测试过一段代码div v-foritem in list v-ifitem.isActive :keyitem.id {{ item.name }} /div在Vue2里这段代码能正常运行v-if会依次判断每个item.isActive。但在Vue3里这段代码会直接抛出错误因为v-if先执行时item还不存在。正确的做法是把v-if放到内层或者用template包一层template v-foritem in list :keyitem.id div v-ifitem.isActive {{ item.name }} /div /template如果只是要根据某个条件过滤列表更推荐的做法是使用computed先过滤再用v-for渲染模板不用写任何判断逻辑。后面我会专门讲这个。如果遇到Vue3里v-if和v-for为什么这样写报错这类问题基本就是优先级搞混了。2.3 key在条件分支里的隐藏作用条件语句还有一个经常被忽略的细节key。有些开发者在v-if切换时发现组件里的数据残留了怎么都重置不干净。这其实不是Vue的bug而是Vue的复用机制在起作用。Vue在渲染DOM时会尽量复用已有的元素以提升性能。但当你在同一个位置上切换两个不同意义的分支时比如登录表单和注册表单它们的输入框结构可能很像。Vue会认为它们是同一个元素于是直接复用导致输入框里保留之前填的内容。解决办法很简单给两个分支加上不同的key强迫Vue把它们当成两个完全不同的节点切换时强制销毁重建form v-ifisLogin keylogin-form input placeholder请输入账号 / /form form v-else keyregister-form input placeholder请输入手机号 / /form这个原理不仅适用于条件分支也适用于v-for列表。key的本质是给虚拟DOM一个身份标识让diff算法能够准确判断哪个节点是新增、哪个是删除、哪个是移动。在条件渲染里它同样会阻止不必要的节点复用。2.4 条件语句里的响应式判断ref、reactive和computed怎么选Vue3引入组合式API之后条件判断里的响应式数据来源变得多样了。用ref定义的基本类型变量在模板里会自动解包所以v-ifvisible可以直接写不用写visible.value。但如果是在script setup里的普通JS逻辑中就必须手动写.value。这看起来很简单实际使用中却有一个高频错误在v-if里写了一个响应式对象的属性但对象本身没有预先声明这个属性。比如const form reactive({ name: , age: 0 })然后在模板里写v-ifform.gender maleform.gender一开始是undefinedVue无法侦测到这个新增属性的变化。不是说你完全不能给响应式对象添加属性而是这种添加不可靠。更好的做法是初始化时就把所有字段声明好或者在条件判断前用hasOwnProperty做一层兜底判断。至于computed它应该承担模板中所有稍微复杂一点的条件逻辑。我见过有人直接在模板里写三元嵌套、多重逻辑或调用方法div v-iflist.filter(i i.type done i.count 10).length 0这段代码每次渲染、每次依赖更新时都会执行完整的过滤和统计完全没有缓存。如果列表很大页面性能会受到明显影响。而computed会根据依赖自动缓存结果只有依赖的响应式数据变化时才会重新计算。条件判断越复杂用computed的收益越大。3. 进阶场景条件渲染在真实业务里的常见玩法3.1 computed筛选列表替代模板里的复杂条件表达式在真实业务里最典型的条件渲染场景就是根据当前筛选条件展示列表。很多人的第一反应是在模板里用v-for加v-if就像我前面说的这在Vue3里还会遇到优先级问题。但最合理的方案其实是把筛选逻辑放到computed里模板只负责遍历最终结果。假设我有一个订单列表需要根据状态和关键词进行筛选const orderList ref([...]) const filterStatus ref(all) const keyword ref() const filteredOrders computed(() { return orderList.value.filter(order { const matchStatus filterStatus.value all || order.status filterStatus.value const matchKeyword !keyword.value || order.orderNo.includes(keyword.value) return matchStatus matchKeyword }) })模板就非常干净div v-fororder in filteredOrders :keyorder.id {{ order.orderNo }} /div这样做有三个好处。第一模板里没有任何业务判断可读性大幅提升第二筛选逻辑可以被单独测试比如在单元测试里直接对filteredOrders做断言第三computed有缓存当orderList没有变化而其他依赖也没变时不会重复执行过滤。这在列表数据量大的情况下性能差距非常明显。我在实际项目里还会把筛选条件封装成可复用的组合式函数比如useFilteredList传入原始列表和筛选参数返回处理后的结果。这样多个页面都能使用同一套筛选逻辑避免重复造轮子。3.2 动态组件与条件渲染组件级的分支复用如果条件判断的粒度不是DOM元素而是整个组件那v-if切换多个组件的方式虽然可行但模板会变得很臃肿。比如一个流程页面根据不同的步骤展示不同的表单组件StepOne v-ifstep 1 / StepTwo v-else-ifstep 2 / StepThree v-else-ifstep 3 /如果步骤多了这就是一堆重复的v-else-if。更优雅的方式是用动态组件component :is...component :iscurrentStepComponent /然后通过一个映射对象来关联步骤和组件import StepOne from ./StepOne.vue import StepTwo from ./StepTwo.vue import StepThree from ./StepThree.vue const stepComponents { 1: StepOne, 2: StepTwo, 3: StepThree } const currentStepComponent computed(() stepComponents[step.value])这样做的好处是新增一个步骤时只需要往映射对象里加一项不需要在模板里再写一行v-else-if。component :is...甚至可以接字符串比如全局注册的组件名也可以接组件选项对象非常灵活。这里有几个细节要注意。动态组件在切换时默认会销毁旧组件再创建新组件所以旧的组件内部状态不会保留。如果需要保留可以配合KeepAlive使用。KeepAlive会让被缓存的组件实例不销毁切换回来时恢复原状态这在做多步骤表单时很实用用户辛苦填的信息不至于因为切换步骤而丢失。3.3 异步组件加Suspense条件判断里的按需加载另一个进阶方向是异步组件。在Vue3里你可以用defineAsyncComponent定义一个异步加载的组件配合Suspense组件实现加载状态的管理。这在条件渲染里很常见某个模块只在特定条件下才展示而这个模块本身又很重比如大型图表库、富文本编辑器、地图组件。我以前做过一个管理后台有一个数据分析面板引入了很重的可视化库如果把整个库都打到主包里面首屏加载时间会多出好几秒。后来改成异步组件只有当用户点击查看图表时才去加载对应的组件和依赖const ChartPanel defineAsyncComponent(() import(./ChartPanel.vue))模板里配合条件判断button clickshowChart true查看图表/button template v-ifshowChart Suspense template #default ChartPanel / /template template #fallback div图表加载中.../div /template /Suspense /template这样初始页面完全不加载图表库用户点开后才发起请求。注意defineAsyncComponent也支持配置loadingComponent和errorComponent但Suspense是组件树级别的加载协调机制两者可以结合使用。对于条件分支里挂载的重组件异步化是提升首屏性能的有效手段。3.4 权限控制与条件渲染做个简单的有权限才显示权限控制在后台管理系统里几乎绕不开。一种常见的做法是用自定义指令v-permission在指令的mounted钩子里检查用户权限没有权限就直接移除元素。但指令方式有一个小问题它操作的是真实DOM如果权限数据是异步获取的指令执行时权限还没加载完元素可能已经被移除了后面即使权限数据更新元素也不会再回来。更可靠的方式是封装一个权限判断的组合式函数配合v-if使用。比如function usePermission() { const permissions ref([]) const hasPermission (code) { return permissions.value.includes(code) } return { permissions, hasPermission } }模板里直接这样写button v-ifhasPermission(order:export)导出订单/button这样权限数据更新后hasPermission的结果会重新计算按钮也会自动出现或隐藏。相比自定义指令这种方式更响应式也更符合Vue3的组合式API风格。如果要做更复杂的权限逻辑比如多个权限满足其一、角色判断都可以在这个组合式函数里扩展。3.5 JSX和渲染函数里的条件逻辑如果你用Vue3写JSXVue 3的vitejs/plugin-vue-jsx条件语句就可以直接用JavaScript的if、三元表达式或逻辑与运算符比模板指令更灵活。const renderButton () { if (loading.value) { return Button disabled加载中.../Button } return Button onClick{handleClick}提交/Button }这种写法在高度动态化的场景里很好用尤其是组件内部逻辑复杂、需要根据多个状态决定渲染结构时JSX的可编程性明显优于模板。但代价是模板的静态分析和编译器优化比如静态节点提升收益会减少所以我的建议是常规业务页面用模板动态渲染需求多、逻辑分支复杂的组件库或高阶组件用JSX。另外如果你使用渲染函数h()条件逻辑和JSX类似也是用JavaScript原生的分支语法来实现。只是h()的写法对于纯前端项目来说可读性不如JSX通常只有组件库开发才会大量使用。4. 常见问题与排查技巧实录4.1 v-if从true变成false后状态丢失怎么解决这是我在社区里被问得最多的问题之一。场景通常是这样的组件里有一个弹窗弹窗里有个表单用户填了一半点了个按钮把v-if的条件变成了false弹窗关闭。再次打开时所有表单数据都清空了用户很恼火。原因前面提过v-if控制下的DOM在条件为false时会被销毁组件实例也会被卸载下次条件为true时会创建全新的组件所以表单数据自然就没了。解决方案有三个方向如果只是临时隐藏且希望保留状态用v-show替换v-if。如果必须是v-if比如组件内有重逻辑不想初始加载可以在外层把组件包进KeepAlive配合include指定缓存哪些组件。更通用的做法是在组件内用watch监听弹窗开关、或暴露一个reset方法在打开弹窗时主动恢复数据或者把数据提升到父组件的ref中让子组件用defineModel或props同步避免数据全部绑定在组件内部。我一般会在弹窗组件里定义一个init方法在弹窗打开时调用这样既保证了每次打开都拿到最新数据又不会因为组件销毁重建导致额外的问题。记住一条原则当一个组件依赖外部条件控制显示时状态管理尽量外提或显式初始化不要依赖组件的自然保留。4.2 条件渲染在v-model上出现的坑先看一段代码input v-ifisEdit v-modelform.name / p v-else{{ form.name }}/p这个看起来没什么但如果isEdit为true时用户输入了内容然后切换isEdit为false再切回true输入的form.name还在因为数据存在form对象里跟DOM是否销毁无关。真正的问题是另外一种场景v-model绑定到了子组件声明的一个内部临时状态而这个子组件本身被v-if控制销毁重建。每次重建内部状态都会回到初始值但父组件传进去的props又是新的结果就出现父组件更新了子组件显示的还是旧值的混乱。解决方法是确保v-model的绑定数据具有唯一数据源不要在子组件内部制造一份副本却不与父组件同步。比如用defineModel或者computed的get/set来桥接让数据始终双向同步。还有一种常见情况是v-model和v-if同时用在一个元素上如果v-if条件依赖另一个未初始化响应式变量会出现undefined相关报错。排查思路是先在setup里把相关变量全部初始化避免模板里对undefined的属性做判断。4.3 环境判断与客户端逻辑浏览器差异带来的条件分支有朋友遇到过这样的问题在Edge浏览器中Vue3项目有时候无法正常关闭浏览器右上角的最小化按钮。这个说起来有点奇怪听着像是浏览器的问题但真去排查时往往会发现项目里有某些逻辑在特定浏览器下没有执行或执行出错。遇到这种跨浏览器的问题我的定位思路是先用条件分支锁定环境差异再逐步缩小范围。比如在代码里根据navigator.userAgent或navigator.userAgentData判断当前浏览器对不同浏览器执行不同的初始化逻辑。但要注意这类判断如果放在了setup顶层而项目中又在服务端渲染或构建阶段执行了这些代码可能直接导致构建失败或警告。所以环境判断一般要放到onMounted里或者用动态导入的方式按需加载。回到那个Edge最小化按钮的问题实际测试中发现这类问题的根源往往不是Vue本身的渲染逻辑而是某些第三方库原生API在Edge下触发了异常导致窗口状态同步逻辑中断。排查的思路还是先看控制台有没有报错再用浏览器原生的调试工具确认是哪一个调用栈引起的。条件语句在跨浏览器方案里更多是用来做特性检测而不是手工区分浏览器版本。特性检测是更可靠的方式比如用showSaveFilePicker in window来判断是否支持某个API而不是直接查浏览器名称。4.4 性能排查频繁切换到底该用哪个我觉得条件渲染最需要考虑性能的场景是那些每秒都可能在切换的界面元素比如播放器控制栏、实时刷新面板、鼠标悬停弹层。如果你在这些场景里用v-if每次切换都涉及DOM节点的创建和销毁会带来可见的卡顿尤其是在低端设备上。反过来如果某个条件永远为false完全不应该渲染的内容你用v-show硬撑着渲染反而浪费初始渲染时间。比如一个只有管理员才能看到的日志抽屉普通用户永远打不开那就不应该用v-show把整个抽屉的DOM渲染出来。判断依据就是条件变化频率高不高如果高用v-show如果低、且初始条件为false用v-if。还有一点容易被忽视v-if与v-show的性能差异不仅仅在DOM操作层面。v-if分支内的组件如果是重型组件富文本编辑器、地图、大型表格每次重新挂载都要执行组件的初始化逻辑包括请求接口、创建实例、初始化状态。这种成本比单纯的DOM节点创建高得多。所以不要只看显示隐藏这个层面要看你分支内部到底承载了多少东西。我通常会在代码审查时给团队成员一个简单标准凡是分支内有组件、表单、图表或复杂交互的默认用v-show凡是分支内只有纯静态展示文本且条件变化极少用v-if其余情况按实际需求判断。4.5 排错方法论条件渲染出问题的时候很多人的第一反应是去模板里东改西改。我自己的习惯是先从数据层确认状态再判断渲染层。推荐在浏览器开发工具里打开Vue DevTools直接查看当前组件的响应式数据状态看v-if依赖的变量到底是不是预期值。如果模板里怎么改都没有反应先检查是不是同一个变量名在模板和script setup里不一致或者变量被reactive包装后在模板里隐式解包导致引用错乱。还有一个高频问题代码里定义了一个const visible ref(false)在某个异步回调里改成了visible.value true但模板里绑定的却是另一个同名变量visible这种情况在多人协作的代码仓库里很容易出现。排查的时候多用IDE的全局搜索确认模板里的变量名和JS里导出的是同一个。如果排查了半天都找不到原因还有一招在模板里临时写一个{{ visible }}直接看渲染结果是不是符合预期。如果模板里能显示正确值但v-if不生效那多半是表达式写错了比如把赋值操作符写成了相等比较。这种低级错误有时候反而最难发现。5. 一点个人实操体会前面聊了这么多最后我想说说我在实际项目里对Vue3条件语句的整体使用心得。第一条件语句不是越少越好也不是越多越不好关键是让条件出现在合适的位置。复杂的判断逻辑应该收敛到computed或组合式函数里模板里尽量只做最基础的分支展示。第二v-if和v-show的选择要考虑业务场景和组件成本不能只凭个人喜好。我在几个项目里尝试过统一规范弹窗类和抽屉类组件优先v-show配合KeepAlive权限类和路由守卫类逻辑用v-if列表筛选和过滤一律用computed。有了这个规范之后团队协作时的沟通成本降低了很多新人也知道什么时候该用哪个指令。第三遇到条件渲染相关的问题先用Vue DevTools确认数据不要急着改模板。数据层面没问题再看模板表达式和指令优先级模板也没问题再考虑是不是组件复用或key的问题。按照这个顺序排查90%的问题都能快速定位。条件语句在Vue3里虽然只是一个基础概念但它和响应式系统、虚拟DOM、组件生命周期都深度绑定。每次你以为自己彻底掌握了总会在某个奇怪的项目里遇到新问题。希望这篇文章里分享的细节和排查经验能帮你少踩一些坑。