做前端的时间久了,会发现一个特别有意思的现象:很多人用 Vue 写了几年业务,v-if、v-for、v-show张口就来,但真到排查线上问题时,翻车的往往是这些最基础的指令。有的是用v-show控制权限节点导致接口泄露,有的是v-for没给 key 造成状态错乱,还有的是在同一个元素上硬怼v-if和v-for,结果渲染结果完全不符合预期。条件渲染和循环渲染,这两个看起来最不起眼的功能,恰恰是 Vue 应用里坑最多的区域之一。
所以这篇东西我不想讲 API 文档里已经写烂的用法,而是想从“为什么这样设计”“什么时候选哪个”“实际项目里怎么组合不踩坑”这几个角度,把条件渲染和循环渲染完整地拆一遍。无论你是刚接触 Vue 没多久的新手,还是写过不少业务的老手,看完之后至少能在下一次写模板时,多问自己一句:这个场景,我到底该用谁,为什么?
1. 条件渲染的底层逻辑与选型标准
条件渲染这个词听起来抽象,落到页面上就一件事:某块 DOM 在什么情况下出现,什么情况下消失。Vue 给我们提供了v-if和v-show两个指令来干这件事,但两者背后的机制完全不一样,选错了后续会埋很多雷。
1.1 v-if 与 v-show 的原理差异
v-if是“真正”的条件渲染。当表达式为假时,Vue 在渲染过程中会直接跳过这段代码,DOM 里根本不存在这个节点。当表达式为真时,才把节点创建出来并插入 DOM。这个过程是惰性的,如果初始条件为 false,元素压根不会渲染,后续即使条件变为 true,它也会重新经历一遍“创建组件实例 → 挂载 → 渲染”的完整流程。
v-show则是“假”的条件渲染。它不管条件真假,元素从一开始就会被渲染到 DOM 里,只是通过 CSS 的display属性在none和原值之间切换。也就是说,v-show控制的是“看不看得见”,而不是“存不存在”。
有个很直观的测试方法:写一个元素分别用v-if和v-show控制,初始都设为 false,打开浏览器开发者工具看 Elements 面板。v-if那边直接是空的,v-show那边可以看到标签安静地躺在 DOM 里,只是有个style="display: none;"。
<div v-if="false">我用的是 v-if,我不在 DOM 里</div> <div v-show="false">我用的是 v-show,我在但看不见</div>这个差异直接决定了它们的开销模型。v-if的切换成本高,因为每次切换都涉及 DOM 的创建或销毁,组件内部的beforeDestroy、destroyed生命周期会依次触发;而v-show的切换成本低,本质上只是改一个 CSS 属性,但它的初始渲染成本是固定的,因为不管条件真假,首次渲染都得把这个节点渲染出来。
1.2 为什么 v-show 不支持 template 标签
用过 Vue 的人应该知道,v-if可以配合原生的<template>标签使用,把多个元素包起来统一控制。比如:
<template v-if="visible"> <p>第一段</p> <p>第二段</p> <p>第三段</p> </template><template>是一个虚拟的包裹标签,最终不会渲染到 DOM 里,三个<p>会根据visible的值统一出现或消失,这种方式非常适合控制一组相邻元素。
但v-show不支持<template>。原因很简单:v-show是靠给元素加display: none来隐藏的,而<template>本身在渲染后不会留下任何 DOM 节点,Vue 没法给“不存在的节点”设置样式。如果你非要写<template v-show="visible">,Vue 会在控制台给你一个警告:v-show无法在<template>上使用,请改用v-if。
实际开发中这个坑不算少见。有些同事想保留v-if的语义,又想要v-show的低切换成本,于是试图用<template v-show>包一组元素,结果发现压根不生效。遇到这种情况别犹豫,直接换v-if,或者把v-show分别写到每个子元素上。
1.3 场景选型:什么时候用 v-if,什么时候用 v-show
很多教程会告诉你一句话:频繁切换用v-show,否则用v-if。这句话没错,但不够细。我补充几个更具体的判断维度:
- 如果初始条件为 false,且这个条件短期内大概率不会变成 true,用
v-if更好。比如用户的权限标识、会员等级这类信息,通常页面加载完就确定了,很少会反反复复变来变去。用v-if还能顺带省掉首次渲染的开销。 - 如果切换频繁,且节点规模不大,用
v-show更合适。典型场景是 Tab 切换、手风琴折叠、下拉菜单展开收起。这类交互可能在几秒内被用户反复触发,用v-if会不断地创建和销毁 DOM,不仅慢,还可能丢失组件内部的状态。 - 如果组件内部有表单输入、定时器、动画,或者组件挂载时有比较重的初始化逻辑(比如请求接口、初始化图表),用
v-if要格外谨慎。因为切换隐藏后组件会被销毁,再次显示时重新执行整个挂载流程,之前填了一半的表单直接清空,定时器没清干净还会造成内存泄漏。
我自己在项目里的一个惯例是:凡是条件在单次页面生命周期内几乎不变、只影响“存不存在”的,一律v-if;凡是用户可能在几秒内反复切换“看不看得见”的,一律v-show。前者比如详情页里根据后端返回状态渲染的支付按钮,后者比如消息中心里展开收起的筛选面板。
2. 循环渲染的核心机制与常见误区
循环渲染就是v-for,用来把数组或对象遍历成一组 DOM。它看着简单,但里面藏着的细节比大多数人以为的要多得多。key 为什么要绑定、数组更新为什么有时候不触发重新渲染、遍历对象和遍历数组有什么区别——这些搞不清楚,列表页迟早出幺蛾子。
2.1 v-for 的完整语法与可遍历的数据类型
v-for的标准写法是v-for="item in items",其中items可以是数组、对象,甚至一个整数。最容易被忽略的是遍历范围值的使用:
<li v-for="n in 10">{{ n }}</li>这会渲染出 1 到 10 共 10 个<li>。实际业务里,这个语法可以用在分页器页码、星级评分这种不需要后端数据驱动的地方,比维护一个[1,2,3,4,5]的数组更直白。
遍历数组时,我们可以拿到两个参数,第二个是索引:
<li v-for="(item, index) in list">{{ index }} - {{ item.name }}</li>遍历对象时,可以拿到的参数更多:首先是值,其次是键,最后是索引:
<li v-for="(value, key, index) in obj">{{ index }}. {{ key }}: {{ value }}</li>这个用起来有个不太显眼的好处:当接口返回的数据结构是对象而不是数组时,不需要在computed里多写一层Object.keys()转换,直接在模板里遍历对象就行。不过要注意,遍历对象的顺序取决于Object.keys()的返回顺序,对于数字键会按键升序排列,对于字符串键则按插入顺序排列。业务中如果需要严格保证顺序,建议还是在数据层把对象转成数组。
ES6 的解构写法在v-for中同样可用,这在处理数组元素是对象时会非常舒服:
<li v-for="{ id, name } in users" :key="id">{{ name }}</li>2.2 key 的作用与就地复用机制
v-for里那个:key不是随便写写就行的,它是 Vue 的 diff 算法在新旧列表对比时的唯一标识。理解这一点,要先明白 Vue 在没有 key 时的“就地复用”策略。
假设你有一个列表,渲染了五个输入框。不给 key,然后你删掉了第二个输入框。Vue 在对比新旧虚拟 DOM 时发现:旧节点的标签类型都一样,内容也差不多,那就直接在原位置复用吧,于是它就偷偷把第三个输入框移到了第二个位置,而不是真正地“按顺序重新渲染”。这个过程对用户来说是感知不到 DOM 变化的,但输入框里的值可能会跟着错位——你在第二个输入框里打了字,删掉第一项后,你打的那行字跑到第三个输入框里去了。
绑定key之后,Vue 就能准确判断出哪个节点是哪个节点,按 key 而不是按索引进行复用和移动。这就像餐厅服务员上菜,如果不看桌号,随便端一盘菜就上,菜可能送错桌;看了桌号(key)就不会错。
key 的取值有个硬性要求:必须是列表项中稳定且唯一的标识。最理想的是后端返回的数据库主键id,其次是uuid、guid这类全局唯一标识。不建议直接拿index当 key,原因是索引会随数组顺序变化而变化,一旦发生增删操作,key 会整体错位,最终导致状态复用混乱。还有一点容易忽略:当列表项本身是静态数据、没有内部状态时,用index其实问题不大;但只要列表项牵涉输入框、复选框、选中态这类可交互状态,就一定要用稳定 id 当 key。
<li v-for="item in list" :key="item.id">{{ item.text }}</li>2.3 数组更新检测的边界情况
Vue 的数据响应式对数组的操作是有“白名单”的。在 Vue 2 中,以下这些方法能触发视图更新:push、pop、shift、unshift、splice、sort、reverse。但如果你直接通过索引赋值,比如this.list[0] = newValue,或者直接修改数组长度,比如this.list.length = 0,视图不会有任何动静。
Vue 2 之所以检测不到,是因为它用的是Object.defineProperty来实现响应式,对数组的拦截是在原型方法层面做的,而通过索引赋值并不会经过这些方法,响应式系统无从感知。
解决办法也很成熟:索引赋值改用this.$set(this.list, index, newValue),或者直接this.list.splice(index, 1, newValue);清空数组用this.list.splice(0)或this.list = []。
到 Vue 3 之后,响应式系统换成了Proxy,直接通过索引赋值和修改 length 都能被侦测到,不用再依赖$set。但即便底层能力增强了,我仍然推荐大家在业务里养成用splice和重新赋值的习惯。原因很简单:代码的可读性更高,而且团队里可能还有同事在维护 Vue 2 的老项目,风格统一能减少切换上下文的心智成本。
2.4 循环里的性能隐患
v-for的另一个问题是性能。渲染一个包含几千个 DOM 节点的列表,Vue 的初始渲染和后续更新都会产生可感知的卡顿。优化手段有两个层面:
首先是减少不必要的渲染。如果列表数据不在当前视口内,可以用虚拟滚动组件(比如vue-virtual-scroller)来保证只渲染可视范围内的节点,这在纯前端渲染十万级数据时几乎是必须的。其次是拆分复杂子组件。如果列表项内部逻辑很复杂,把每一项抽成独立的子组件,当列表更新时,Vue 只需要更新真正变化的那个子组件,而不是把父组件的整个v-for块重新渲染一遍。
有一点要特别注意:不要在v-for里写复杂的表达式或方法调用。比如<li v-for="item in list">{{ formatPrice(item.price) }}</li>,每次父组件重新渲染时,这个formatPrice都会被调用 N 次。如果formatPrice内部逻辑较重,或者依赖的数据源复杂,N 次调用的开销会迅速放大。推荐的做法是把格式化逻辑提前在computed或数据层处理好,模板里只做展示。
3. v-if 与 v-for 同元素共存的优先级较量
这大概是 Vue 模版中争议最多、最容易被忽略规则的地方。很多工程师在同一个元素上写v-if和v-for,原因是觉得“循环遍历每一项,同时判断是否满足条件”,听起来很自然。但 Vue 对这两个指令同时存在时是有固定处理顺序的,而且 Vue 2 和 Vue 3 的顺序恰好相反。
3.1 Vue 2 中 v-for 优先于 v-if
在 Vue 2 中,当v-if与v-for用在同一元素上时,v-for会先执行,v-if再对每一项进行判断。也就是说,Vue 会先把整个列表遍历一遍,生成完整的虚拟节点列表,然后在渲染前用v-if逐个过滤。这个过程有两个直接后果:
- 如果列表有 100 项,其中 98 项不满足条件,Vue 依然会遍历 100 次,每次都要执行
v-if表达式。性能上白耗了 98 次无效计算。 - 由于循环在判断之前,
v-if的条件下根本拿不到v-for当前迭代的item。如果你想用v-if="item.isShow"来过滤,Vue 2 里这个item是 undefined,表达式直接报错。
第二个坑我在实际项目里见人踩过不少次。很多新手写的代码长这样:
<li v-for="item in list" v-if="item.isActive" :key="item.id">{{ item.name }}</li>在 Vue 2 中,这段代码不会报语法错误,但逻辑上是反的——v-if是在v-for的每一轮迭代里判断的,所以item其实是拿得到的。这里真正的痛点在于性能:哪怕只有少数项满足条件,Vue 仍然会跑完整个循环。
对于过滤场景,官方的推荐做法是把v-if上移到外层<template>,或者干脆用计算属性提前过滤。比如:
<template v-if="showActiveOnly"> <li v-for="item in activeList" :key="item.id">{{ item.name }}</li> </template>而在 Vue 2 中,如果希望v-if拿到item做过滤,就要用嵌套<template>或者把逻辑提前到computed,直接过滤出最终要渲染的列表。
3.2 Vue 3 中 v-if 优先于 v-for
Vue 3 把两者的优先级对调了:v-if优先于v-for。这意味着当你写<li v-for="item in list" v-if="condition">时,Vue 会先判断condition,如果为 false,整个循环根本不会执行;如果为 true,再进入v-for遍历。
这带来一个非常实际的问题:在 Vue 3 中,v-if表达式里如果想使用v-for的item,是不行的。因为v-for还没执行,item根本不存在。官网也明确给出了警告建议:不要在同一元素上同时使用v-if和v-for,宁可多加一层<template>,也不要依赖这项优先级。
例如 Vue 3 下这种写法就是有问题的:
<li v-for="item in list" v-if="item.isActive" :key="item.id">{{ item.name }}</li>v-if="item.isActive"中的item在v-if先执行时尚未定义,运行时直接抛错或者渲染结果不符合预期。正确的写法是:
<template v-for="item in list" :key="item.id"> <li v-if="item.isActive">{{ item.name }}</li> </template>或者更推荐的过滤方案:在computed里先filter一遍,模板只保留v-for。这样既避免了指令优先级纠缠,也让模板更干净。
3.3 正确的过滤姿势:computed 先行
不管是 Vue 2 还是 Vue 3,在同元素上混合v-if和v-for都称不上好习惯。最干净的方案是把过滤提前到 JavaScript 层面,用计算属性完成。这样有几个好处:
- 模板里的逻辑更单一,
v-for只负责“渲染”,v-if只负责“条件展示”,两者各司其职。 - 过滤结果会被缓存。当列表数据不变时,
computed不会重新执行,比在模板里每次渲染都跑一遍过滤高效得多。 - 代码的可测试性更好。过滤逻辑从模板抽离到 JS,可以单独写单元测试,这在业务复杂时非常实用。
const activeUsers = computed(() => { return users.value.filter(user => user.isActive) })<li v-for="user in activeUsers" :key="user.id">{{ user.name }}</li>这样写,模板里只剩下纯粹的数据展示,谁来了都看得懂。我在 code review 时看到同元素混用的写法,第一反应永远是“为什么不把这个过滤放到 computed 里”。
3.4 同元素共存的例外场景
当然,不是所有“v-if + v-for”组合都是反模式。有一种场景很适合把两者放一起:小规模、静态列表的简单判断。比如导航菜单里有 3 个固定项,想根据用户角色控制其中一项是否展示,此时列表只有 3 项,性能开销可忽略,而且数据写死在模板里,不值得为它专门写一个 computed。
<nav> <a v-for="link in links" v-if="link.visible" :key="link.label">{{ link.label }}</a> </nav>这种情况下,代码直观性和可读性反而比引入 computed 更强。但前提是:列表规模小、条件简单、没有复杂的依赖。一旦列表可能动态增长,或者过滤条件依赖外部响应式数据,还是回到 computed 方案更稳。
4. 条件与循环组合的实战场景拆解
脱离场景聊 API 没有意义。下面我用几个真实开发里经常遇到的需求,把v-if、v-show、v-for如何组合、如何取舍的过程完整走一遍。
4.1 场景一:权限控制下的按钮组渲染
需求是角色 A 能看到操作列里的“编辑”“删除”按钮,角色 B 只能看到“查看详情”。后端会返回一个权限标识集合,前端要根据标识动态决定按钮显隐。
这个场景有两个候选方案:v-if或v-show。如果只是控制“看不看得见”,两个都能实现,但权限控制本质上是不该被看到的逻辑就不该存在——用v-show隐藏按钮,只是把按钮藏在了 CSS 里,浏览器 DOM 里按钮还在,用户打开开发者工具照样能看到按钮结构,甚至可以通过修改display强行触发点击事件。从安全角度考虑,权限控制必须用v-if从结构上移除。
<div class="action-bar"> <button v-if="canView">查看</button> <button v-if="canEdit">编辑</button> <button v-if="canDelete">删除</button> </div>这里还有个容易忽略的细节:权限判断逻辑如果直接写在模板里,会重复出现很多次。合理的做法是把权限判断收敛到一个computed或permission.js工具模块里,模板里只暴露布尔值。比如:
const canEdit = computed(() => permissionList.value.includes('edit')) const canDelete = computed(() => permissionList.value.includes('delete'))这样模板干净,权限规则修改时也只动一个文件。
4.2 场景二:Tab 切换中的内容保留
需求是页面顶部有三个 Tab,点击切换时对应当前 Tab 显示内容区域。问题是:A Tab 里有一个填了一半的表单,切到 B 再切回来,填的内容能不能保留?
如果用v-if控制内容区,A Tab 被切走时组件会销毁,表单数据跟着消失,切回来时重新走挂载流程,数据重新初始化,用户填的内容全没了。如果用v-show,A Tab 只是被display: none隐藏,组件实例和表单状态都还在,切回来时原样呈现。
这个场景用v-show是对的,但要注意性能:如果 Tab 页里的子组件渲染很重、数据拉取开销很大,第一次渲染时会全部加载,这在某些场景下不可接受。折中方案是懒渲染 + 缓存:首次访问某个 Tab 时才让组件挂载,挂载后就不销毁,切换时用v-show隐藏。实现方式可以配合<keep-alive>和动态组件,或者在组件里通过activated钩子控制数据加载时机。
这里我推荐一个成熟做法:用动态组件配上v-show标志位,首次激活时才渲染对应组件。实际操作时用一个 Map 记录已激活的 Tab,模板里判断当前激活的 Tab 决定渲染哪个组件,同时已激活的组件不销毁。
4.3 场景三:列表页的加载中、空数据、加载失败与正常态
一个从接口拉取的列表页,至少要处理四种状态:加载中、请求失败、数据为空、有数据。这四种状态的切换本质上就是一个四选一的条件渲染问题。
最直观的写法是用一个status字段控制:
<div v-if="status === 'loading'">加载中...</div> <div v-else-if="status === 'error'"> 加载失败,<button @click="fetchData">重试</button> </div> <div v-else-if="status === 'empty'">暂无数据</div> <ul v-else> <li v-for="item in list" :key="item.id">{{ item.name }}</li> </ul>这里v-if是绝对正确的选择:四种状态在同一时间只有一种生效,并且各个状态对应的 DOM 结构差异很大,根本不存在“切换成本”的顾虑。v-show在结构差异大时必须同时渲染四个块,白白浪费 DOM 节点。另外注意这种多分支场景下,v-else-if和v-else必须紧跟在对应的v-if元素之后,中间不能插入其他元素,否则 Vue 无法建立正确的分支链。
4.4 场景四:嵌套循环与分组渲染
需要渲染一个分组的商品列表:每个分类下有多个商品。数据结构通常是这样的:
[{ categoryName: '水果', products: [{ name: '苹果' }, { name: '香蕉' }] }, { categoryName: '零食', products: [{ name: '薯片' }] }]模板层用两层v-for嵌套即可:
<div v-for="group in groupedList" :key="group.categoryId"> <h3>{{ group.categoryName }}</h3> <ul> <li v-for="product in group.products" :key="product.id">{{ product.name }}</li> </ul> </div>嵌套循环的关键是 key 不能省。外层 key 用分类 id,内层 key 用商品 id,两层各自独立。如果商品跨分类移动,只要 key 稳定,Vue 就能精确识别元素的移动而不是重建整个 DOM 树。这里还有一个性能小技巧:当商品项内部交互复杂时,把内层列表抽成独立子组件,并把分组数据作为 prop 传入,这样外层分组变化时不会重渲染整个商品列表。
4.5 场景五:搜索筛选与动态展示
需求是把用户列表按角色筛选,展示当前角色对应的用户。用户可能切换角色,也可能调整关键字。这个场景我见过不少同事直接用模板层过滤:
<li v-for="user in filteredList" :key="user.id">{{ user.name }}</li>其中filteredList在computed里写:
const filteredList = computed(() => { let result = users.value if (selectedRole.value !== 'all') { result = result.filter(user => user.role === selectedRole.value) } if (keyword.value) { result = result.filter(user => user.name.includes(keyword.value)) } return result })这个方案的可贵之处在于:所有过滤逻辑都集中在computed里,模板只有一行v-for。修改筛选条件时只改一处,调试时只要检查computed的返回结果。性能上computed有缓存,只有当依赖的users、selectedRole、keyword变化时才会重新计算,比模板内嵌过滤表达式高效得多。
5. 实际项目中高频踩坑与排查技巧
这一节梳理我在真实项目里遇到的最典型问题,以及对应的排查思路。这些坑都不是什么高深难题,但一旦发生,排查起来往往很费时间。
5.1 控制台报 “Duplicate keys found”
这个报错的意思是同一层级的v-for循环中出现了重复的 key。最常见的原因是后端接口返回的数据里,主键字段不是唯一的;或者前端用了index当 key,同时数据被截断、拼接、排序,导致索引错乱。
排查方法是先到 network 面板看接口返回的原始数据,确认id字段是否真的唯一。如果接口确实返回重复 id,最稳妥的办法是在前端组装数据时补充一个唯一的业务标识,比如${id}-${index}。对于列表排序后索引会变化的问题,归根结底还是要用稳定 id。另外,当v-for遍历对象时,如果对象的 key 本身不是唯一,也可能触发这个报错,注意区分。
5.2 输入框内容被“串位”
列表中有输入框时,删除或新增列表项后,输入框里的文字跑到了其他行。这个问题我在前面提过:根因是没有绑定稳定的 key,或者 key 用了index。
Vue 就地复用时,会尽量复用旧的 DOM 节点而不重建。当列表项发生增删时,没有稳定 key 的节点就会发生错位,输入框和它的 value 没有一起移动。修复方式就是给列表项一个稳定且唯一的 key,让 Vue 能准确追踪每一项。如果列表项确实没有稳定 id,可以考虑在使用index的基础上,给输入框绑定v-model到数组元素对象的字段上,确保错位时 value 随对象走,而不是留在 DOM 上。
5.3 v-if 切换后组件状态丢失
场景是折叠面板展开后有输入框,用户填了内容,折叠后再展开,发现输入框内容消失。原因是我前面反复强调的:v-if切换会销毁和重建组件。
如果你希望保留状态,换v-show是第一步。但如果折叠面板很多、内容很重,全用v-show会导致首次渲染开销过大。此时的进阶方案是用<keep-alive>配合动态组件缓存组件实例。Vue 内置的<keep-alive>可以让组件在首次挂载后进入缓存,切走后不销毁,重新显示时直接激活缓存实例,表单内容和滚动位置都能保留。
<keep-alive> <component :is="currentTabComponent" /> </keep-alive>5.4 列表更新后视图不刷新
这是 Vue 2 项目里非常经典的问题:修改了数组某个元素的值,或者给对象新增/删除了属性,视图没有反应。前面已经解释过根因:Vue 2 的响应式系统对这些操作是检测不到的。
排查思路很简单:先确认是 Vue 2 还是 Vue 3 项目。如果是 Vue 2,直接调用this.$forceUpdate()不是真正解决问题,正确做法是使用this.$set或splice让响应式系统感知到变化。给对象新增属性也是一样,用this.$set(this.obj, 'newKey', value)。Vue 3 项目里这类问题大幅减少,但也要小心:如果你用了Object.freeze()冻结了数据源,那么任何修改都无法触发视图更新。
5.5 v-for 里所有选项都渲染了同一份数据
这个坑比较隐蔽。当一个v-for循环里的<option>或子组件接收到完全一样的 props 时,通常是因为外部传入的数组实际上是同一个引用。比如:
const list = ref([]) for (let i = 0; i < 5; i++) { list.value.push(defaultItem) // defaultItem 是同一个对象引用 }每项 push 的都是同一个defaultItem,循环里渲染时拿到的都是同一个对象,任何修改都会互相影响。解决方案是每次 push 时创建新对象:{ ...defaultItem }。这个问题在表格编辑、下拉框选项等场景经常发生,排查时先把list打印出来按项比较引用即可确认。
5.6 大数据量列表的卡顿优化
最后说一个性能排查话题。当v-for渲染上万条数据时,浏览器会出现明显的滚动卡顿,这是 DOM 节点过多导致的渲染压力。优化思路有两种:
- 时间分片:把大批量数据的插入分散到多个帧中执行,减少单帧主线程压力。
- 虚拟滚动:只渲染用户可见区域内的节点,滚动时动态替换。这是目前最常用的方案,第三方库足够成熟,能支撑几十万条数据的流畅滚动。
实际做优化之前,先打开 Performance 面板录制一段滚动操作,确认瓶颈到底是 JavaScript 执行时间还是渲染层。如果瓶颈在数据更新,优先检查 computed 和 watcher 是否有重复计算;如果瓶颈在 DOM 节点数,虚拟滚动是正解。别一上来就整虚拟滚动,先排除自身代码逻辑问题,这个原则能省很多事。
6. 把条件渲染和循环渲染写进团队规范
Vue 的条件渲染和循环渲染从来不缺官方文档的说明,但真正让一个团队少踩坑的,往往不是文档,而是沉淀在项目里的一套约定。分享几个我在团队里推行、且实测有效的习惯,都围绕这两个功能展开。
第一,模板里不同时写v-if和v-for。如果遇到过滤需求,一律在computed里完成。模板里单条指令的职责越单一,出问题的概率越低。这条规则基本能消灭 90% 的同元素指令冲突问题。
第二,v-for的 key 必须稳定且唯一。没有稳定 id 时,前端生成一个clientId并缓存下来,比直接拿index硬顶要稳得多。这条规则主要保护的是列表有增删排序时的状态一致性。
第三,能少渲染就别多渲染。初始时不需要展示的内容用v-if,频繁切换的轻量内容用v-show,两者之间没有模糊地带时,优先选v-if。这条规则的潜台词是:为性能负责的是我们自己,不是框架。
第四,列表项内部逻辑复杂时抽子组件。多抽一层子组件不仅提升复用性,还能利用 Vue 的组件级更新机制,避免父组件渲染带动整个列表重渲。
第五,涉及权限、敏感数据的显隐控制,禁用v-show,强制v-if。这是我反复强调的安全底线,不只是性能和体验的问题。
把这些规则写进团队的 code review 清单后,我在评审里看到相关问题的频率明显下降。倒不是说新人都不踩坑了,而是规则有了出处,讨论时不用再纠结“这样写能不能跑”,而是关注“这样写是否符合约定”。对于长期维护的项目,这种统一性比任何单一技巧都更有价值。