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

Vue3里computed和v-for一起用要注意啥?常见场景咋处理?

terry 20小时前 阅读数 16 #SEO
文章标签 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只在listsearchKey变化时重新计算,而methods里的filterItem每次组件渲染(哪怕是其他无关数据变化)都会执行,如果列表有1000条数据,methods的重复计算能把页面卡成PPT…

v-for 里用 computed 怎么写最规范?

核心逻辑是:用computed返回“处理后的新数组”,v-for直接循环这个数组,步骤分这几步:

  1. 定义原始响应式数据(用ref或reactive)。
  2. 写computed函数,基于原始数据做过滤、映射、分组等操作,返回新数组。
  3. 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前端网发表,如需转载,请注明页面地址。

热门