Vue3里computed和v-for一起用要注意啥?常见场景咋处理?
很多刚上手Vue3的同学,写列表渲染时想对数据做过滤、排序或者衍生计算,就会纠结computed和v-for咋配合,今天咱从场景、写法、性能这些角度掰扯清楚~
computed 和 v-for 结合是啥场景?
简单说,当你需要基于原始数据生成“加工后”的列表,再用v-for渲染时,就会用到这俩的组合,常见场景比如:
- 「列表过滤」:原始数组里挑出符合条件的项(比如显示未完成的todo)。
- 「数据衍生」:给原始数据加额外计算属性(比如给商品列表每个项算折扣后价格)。
- 「分组渲染」:把一维数组按规则拆成二维数组再循环(比如按字母首拼分组联系人)。
举个直观例子:后台返回一堆商品数据,前端要只显示“库存>0”的商品,还得给每个商品算“折扣后价格”,这时候用computed先把这堆逻辑处理成新数组,v-for直接循环这个加工好的数组,代码会更干净。
为啥不用 methods 非要用 computed?
很多同学会想:“我在v-for里调用methods里的函数处理数据不行吗?” 还真不一样——computed有缓存,methods每次调用都重新执行。
比如你做一个“搜索过滤列表”功能,用户输入关键词时,computed依赖的searchKeyword变了才会重新计算过滤后的数组;但如果用methods,只要组件重新渲染(比如其他数据变化),v-for里的methods函数就会被反复调用,要是列表数据量大,性能差距会很明显。
举个代码对比:
<!-- 用 computed 更高效 -->
<template>
<ul>
<li v-for="item in filteredList" :key="item.id">{{ item.name }}</li>
</ul>
</template>
<script setup>
import { ref, computed } from 'vue'
const list = ref([/* 原始数据 */])
const searchKey = ref('')
const filteredList = computed(() => {
return list.value.filter(item => item.name.includes(searchKey.value))
})
</script>
<!-- 用 methods 可能拖慢性能 -->
<template>
<ul>
<li v-for="item in list" :key="item.id">
{{ filterItem(item) }} <!-- 每次渲染都执行filterItem -->
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const list = ref([/* 原始数据 */])
const searchKey = ref('')
function filterItem(item) {
return item.name.includes(searchKey.value) ? item : null
}
</script>
能看到,computed只在list或searchKey变化时重新计算,而methods里的filterItem每次组件渲染(哪怕是其他无关数据变化)都会执行,如果列表有1000条数据,methods的重复计算能把页面卡成PPT…
v-for 里用 computed 怎么写最规范?
核心逻辑是:用computed返回“处理后的新数组”,v-for直接循环这个数组,步骤分这几步:
- 定义原始响应式数据(用ref或reactive)。
- 写computed函数,基于原始数据做过滤、映射、分组等操作,返回新数组。
- v-for循环computed返回的数组,记得加唯一key(避免DOM乱跳)。
看个“ todo 列表只显示未完成项”的例子:
<template>
<div>
<!-- 循环computed返回的数组 -->
<ul>
<li v-for="todo in unCompletedTodos" :key="todo.id">
{{ todo.title }}
<button @click="todo.done = true">标记完成</button>
</li>
</ul>
<!-- 显示原始数据总数和未完成数 -->
<p>原始todo数:{{ todos.length }}</p>
<p>未完成todo数:{{ unCompletedTodos.length }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 原始数据
const todos = ref([
{ id: 1, title: '买咖啡', done: false },
{ id: 2, title: '写代码', done: false },
{ id: 3, title: '交周报', done: true }
])
// computed处理:过滤出done为false的项
const unCompletedTodos = computed(() => {
return todos.value.filter(todo => !todo.done)
})
</script>
这里有个细节:computed里的操作要“纯”——别在computed里做异步请求、修改原始数据这些副作用操作,因为computed设计是用来做“计算衍生值”的,不是执行命令式逻辑。
遇到列表嵌套或者复杂计算,computed 咋处理?
实际项目里,数据结构可能很复杂,二维数组分组”或者“给每个列表项加多层计算”,这时候要注意分层处理,别把computed写得太臃肿。
举个“按首字母分组联系人”的例子(原始数据是一维数组,要按name首字母分成多组):
<template>
<div v-for="(group, letter) in groupedContacts" :key="letter">
<h3>{{ letter }}</h3>
<ul>
<li v-for="contact in group" :key="contact.id">{{ contact.name }}</li>
</ul>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 原始联系人数据
const contacts = ref([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Amy' },
{ id: 4, name: 'Ben' }
])
// computed做分组:按name首字母分组
const groupedContacts = computed(() => {
const groupObj = {}
contacts.value.forEach(contact => {
const firstLetter = contact.name[0].toUpperCase()
if (!groupObj[firstLetter]) {
groupObj[firstLetter] = []
}
groupObj[firstLetter].push(contact)
})
return groupObj
})
</script>
这里computed返回的是对象(键是首字母,值是数组),外层v-for循环对象的键值对,内层v-for循环每个分组的数组,这种嵌套场景下,computed负责“数据重组”,模板只负责渲染,代码分层很清晰。
要是计算逻辑更复杂(比如还要筛掉已删除联系人、按年龄排序再分组),可以把复杂逻辑拆成多个computed,比如先写一个activeContacts过滤掉已删除的,再写groupedActiveContacts基于activeContacts做分组——这样每个computed职责单一,好维护,还能利用缓存减少重复计算。
性能坑点有哪些?怎么避?
哪怕用了computed,写不对也会踩性能坑,这几个点要重点关注:
避免在computed里做“无意义的深拷贝/深遍历”
比如你为了“保险”,每次computed都把原始数组深拷贝一遍再处理,这会让计算开销陡增,除非真的需要修改原始数据(但computed里不能改!),否则直接操作原始数组的引用即可。
坏例子(多余深拷贝):
const badComputed = computed(() => {
// 没必要!原始数组是响应式的,直接filter就好
const copy = JSON.parse(JSON.stringify(list.value))
return copy.filter(...)
})
v-for 的 key 必须用唯一稳定值
如果v-for循环的是computed返回的数组,key要用数据本身的唯一标识(比如id),别用索引!否则列表项顺序变化时,Vue无法高效diff,会重复销毁/创建DOM,拖慢性能。
坏例子(用索引当key):
<li v-for="(item, index) in filteredList" :key="index">...</li>
警惕“隐式依赖”导致的不必要计算
Vue3的computed是基于响应式依赖自动追踪的,但如果在computed里用了非响应式数据,或者依赖了没被正确追踪的属性,可能导致计算不及时或者重复计算。
比如你把原始数据存在普通变量里(不是ref/reactive),computed依赖它就会失效:
// 错误:list不是响应式的,computed无法追踪变化 let list = [/* 数据 */] const badComputed = computed(() => list.filter(...)) // 改成ref包裹 const list = ref([/* 数据 */])
实战案例:用 computed + v-for 做 todo 列表过滤+统计
光说不练假把式,咱做个能“切换显示全部/未完成/已完成”的todo列表,看computed和v-for咋配合。
需求:
- 显示todo列表,每条todo有“标题”和“完成状态”。
- 有三个按钮:显示全部、显示未完成、显示已完成。
- 底部显示当前tab下的todo数量。
代码实现:
<template>
<div class="todo-app">
<!-- 切换tab -->
<div class="tabs">
<button
@click="activeTab = 'all'"
:class="{ active: activeTab === 'all' }"
>全部</button>
<button
@click="activeTab = 'uncompleted'"
:class="{ active: activeTab === 'uncompleted' }"
>未完成</button>
<button
@click="activeTab = 'completed'"
:class="{ active: activeTab === 'completed' }"
>已完成</button>
</div>
<!-- 渲染todo列表 -->
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ done: todo.done }"
>
{{ todo.title }}
<button @click="toggleTodo(todo)">
{{ todo.done ? '标记未完成' : '标记完成' }}
</button>
</li>
</ul>
<!-- 统计数量 -->
<p class="count">当前显示 {{ filteredTodos.length }} 条todo</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 原始todo数据
const todos = ref([
{ id: 1, title: '学习Vue3', done: false },
{ id: 2, title: '写技术文章', done: false },
{ id: 3, title: '健身', done: true },
{ id: 4, title: '读小说', done: true }
])
// 激活的tab
const activeTab = ref('all')
// computed根据tab过滤数据
const filteredTodos = computed(() => {
if (activeTab.value === 'all') {
return todos.value
} else if (activeTab.value === 'uncompleted') {
return todos.value.filter(todo => !todo.done)
} else { // completed
return todos.value.filter(todo => todo.done)
}
})
// 切换完成状态的方法
function toggleTodo(todo) {
todo.done = !todo.done
}
</script>
<style scoped>
.todo-app { padding: 20px; }
.tabs button { margin-right: 10px; }
.active { color: blue; font-weight: bold; }
.todo-list li { margin: 10px 0; }
.done { text-decoration: line-through; opacity: 0.5; }
.count { margin-top: 20px; }
</style>
逻辑拆解:
todos是原始响应式数据,存所有todo。activeTab控制当前显示的tab,是computed的依赖之一。filteredTodos根据activeTab的值,返回对应过滤后的数组。- v-for循环
filteredTodos,渲染对应的todo项。 - 点击“标记完成/未完成”时,直接修改todo的
done属性(因为todo在响应式数组里,Vue能追踪变化,触发computed重新计算)。
这个案例里,computed自动处理了“tab切换”和“todo状态变化”时的列表更新,而v-for只负责渲染最终的数组,代码逻辑和渲染层分离得很干净~
和 reactive、ref 结合时要注意啥?
Vue3里响应式数据分ref(基本类型/单值对象)和reactive(对象/数组),和computed结合时,关键是让computed能正确追踪依赖。
ref 包裹的数组/对象
如果用ref存数组(比如const list = ref([])),在computed里要访问list.value才能拿到响应式数据,要是漏写.value,computed就拿不到响应式依赖,数据变化时不会重新计算。
正确写法:
const list = ref([/* 数据 */])
const computedData = computed(() => {
return list.value.filter(...) // 必须用 .value
})
reactive 包裹的对象里的数组
如果用reactive存对象,里面包含数组(比如const state = reactive({ list: [] })),computed里直接访问state.list即可,因为reactive的属性是自动解包的。
例子:
const state = reactive({
list: [/* 数据 */],
search: ''
})
const filteredList = computed(() => {
return state.list.filter(item => item.name.includes(state.search))
})
避免在computed里修改响应式数据
computed的函数要保持“纯”,只做计算不做修改,如果在computed里push数据、改属性,会触发无限循环(因为修改数据会让computed重新计算,重新计算又修改数据…)。
错误例子(别这么写!):
const badComputed = computed(() => {
// 错误:在computed里修改原始数据
todos.value.push({ id: 99, title: '新增项' })
return todos.value.filter(...)
})
总结一下
Vue3里computed和v-for的配合,核心是用computed做“数据加工”,v-for做“视图渲染”,既利用了computed的缓存提升性能,又让模板代码更简洁,记住这几个关键点:
- 场景匹配:过滤、衍生、分组这些“基于原始数据生成新列表”的场景优先用。
- 写法规范:computed返回新数组,v-for循环它,key用唯一标识。
- 性能优化:避免无意义深拷贝,合理拆分computed,重视key的作用。
- 响应式结合:ref要加.value,reactive直接访问属性,别在computed里改数据。
实际项目里多练几个案例(比如商品列表过滤+价格计算、评论列表按时间分组),自然就掌握住精髓啦~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


