为什么Vue 3的计算属性能解决模板臃肿、性能优化和双向同步三大痛点?

一、计算属性的基本使用

计算属性(Computed)是Vue 3中用于派生状态的核心API,它能将复杂的逻辑从模板中抽离,让代码更简洁、可维护。

1.1 为什么需要计算属性?

模板表达式(如{{ author.books.length > 0 ? 'Yes' : 'No' }})适合简单逻辑,但如果逻辑复杂或需要重复使用,直接写在模板里会有两个问题:

  • 模板臃肿:大量逻辑会让模板难以阅读;
  • 代码冗余:同一逻辑重复写多次,维护成本高。

计算属性的作用就是将派生逻辑封装成"虚拟属性",让模板只负责展示,逻辑交给计算属性处理。

1.2 Options API 中的计算属性

在Options API中,计算属性通过computed选项定义,它是一个对象,键是计算属性名,值是getter函数(用于计算值)。

示例:判断作者是否有出版书籍

javascript 复制代码
export default {
  data() {
    return {
      author: {
        name: 'John Doe',
        books: ['Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery']
      }
    }
  },
  computed: {
    // 计算属性的getter函数
    publishedBooksMessage() {
      // this 指向组件实例,自动追踪author.books的变化
      return this.author.books.length > 0 ? 'Yes' : 'No'
    }
  }
}

模板中直接使用计算属性,和普通data属性一样:

vue 复制代码
<template>
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span>
</template>

1.3 Composition API 中的计算属性(<script setup>

在Composition API中,使用computed()函数创建计算属性,返回的是computed ref(可响应的引用类型)。

示例(与上面功能一致):

vue 复制代码
<script setup>
import { reactive, computed } from 'vue'

// 响应式数据:作者信息
const author = reactive({
  name: 'John Doe',
  books: ['Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery']
})

// 计算属性:判断是否有书籍
const publishedBooksMessage = computed(() => {
  // 依赖author.books,自动追踪变化
  return author.books.length > 0 ? 'Yes' : 'No'
})
</script>

<template>
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span> <!-- 自动解包,无需.value -->
</template>

注意

  • computed()函数接收一个getter函数 ,返回的publishedBooksMessageComputedRef类型;
  • 在模板中,computed ref会自动解包 (不用写.value);
  • <script>中访问时,需要用.value(如publishedBooksMessage.value)。

二、计算属性的缓存机制:与方法的本质区别

计算属性和方法都能实现逻辑复用,但缓存机制是它们的核心区别。

2.1 缓存的意义:性能优化

计算属性的结果会基于依赖自动缓存------只有当依赖的响应式数据变化时,才会重新计算;否则直接返回缓存值。

而方法没有缓存,每次调用(如模板渲染、函数调用)都会重新执行逻辑。
往期文章归档

示例:对比计算属性和方法

javascript 复制代码
// 计算属性:缓存结果
const expensiveComputed = computed(() => {
  console.log('计算属性执行了')
  return heavyCalculation() // 假设这是一个耗时操作
})

// 方法:无缓存
function expensiveMethod() {
  console.log('方法执行了')
  return heavyCalculation()
}
  • 当依赖不变时,多次访问expensiveComputed.value,只会打印一次"计算属性执行了";
  • 多次调用expensiveMethod(),每次都会打印"方法执行了"。

2.2 依赖追踪:只更新必要的计算

Vue会自动追踪计算属性的依赖(即getter函数中用到的响应式数据),只有依赖变化时,计算属性才会重新计算。

反例:依赖非响应式数据

javascript 复制代码
const now = computed(() => Date.now()) // Date.now()不是响应式数据

无论过多久,now.value都不会更新------因为Vue无法追踪Date.now()的变化。

三、可写计算属性:处理双向逻辑

默认情况下,计算属性是只读 的(只有getter),但在某些场景下,我们需要通过计算属性反向修改源数据 (如fullName同步firstNamelastName),这时可以用可写计算属性(同时定义getter和setter)。

3.1 场景需求:双向同步

比如,我们有firstNamelastName两个响应式数据,希望通过fullName(如"John Doe")同时读取和修改它们:

  • 读取fullName时,返回firstName + ' ' + lastName
  • 修改fullName时,将新值拆分成firstNamelastName

3.2 可写计算属性的实现

可写计算属性需要同时定义get(读取逻辑)和set(修改逻辑)。

Options API 写法

javascript 复制代码
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    fullName: {
      // getter:组合firstName和lastName
      get() {
        return this.firstName + ' ' + this.lastName
      },
      // setter:拆分新值到firstName和lastName
      set(newValue) {
        [this.firstName, this.lastName] = newValue.split(' ')
      }
    }
  }
}

Composition API 写法

vue 复制代码
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  // getter:组合firstName和lastName
  get() {
    return firstName.value + ' ' + lastName.value
  },
  // setter:拆分新值到firstName和lastName
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})
</script>

使用示例

javascript 复制代码
// 修改fullName,会同步更新firstName和lastName
fullName.value = 'Jane Smith'
console.log(firstName.value) // Jane
console.log(lastName.value)  // Smith

四、获取计算属性的之前值

Vue 3.4新增了获取计算属性之前值 的能力,通过computed()函数的previous参数实现。这在需要保留历史状态的场景中很有用(如"只能减小不能超过某个值")。

4.1 应用场景:保留历史状态

比如,我们希望alwaysSmall始终返回count的值,但当count超过3时,保持之前的最大值(3):

vue 复制代码
<script setup>
import { ref, computed } from 'vue'

const count = ref(2)
const alwaysSmall = computed((previous) => {
  if (count.value <= 3) {
    return count.value // 当count≤3时,返回当前值
  }
  return previous // 当count>3时,返回之前的value(最大是3)
})
</script>

效果

  • count从2→3→4时,alwaysSmall.value依次是2→3→3;
  • count从4→2时,alwaysSmall.value变回2。

五、计算属性的最佳实践

为了避免踩坑,使用计算属性时需遵循以下规则:

5.1 保持 getter 无副作用

计算属性的getter应该是纯函数------只依赖输入计算输出,不修改任何外部状态(如修改其他响应式数据、发送请求、操作DOM)。

反例(错误写法):

javascript 复制代码
const badComputed = computed(() => {
  this.author.name = 'Jane' // 错误:修改了其他状态
  return this.author.books.length > 0 ? 'Yes' : 'No'
})

这样会导致逻辑混乱,因为getter的职责是计算值 ,而不是修改状态 。修改状态的操作应该放在methodswatch中。

5.2 不要直接修改计算属性的值

计算属性是派生状态(由源数据计算而来),直接修改计算属性的值是没有意义的------因为它会被下一次计算覆盖。

反例(错误写法):

javascript 复制代码
// publishedBooksMessage是计算属性(无setter)
publishedBooksMessage.value = 'No' // 错误:只读属性

正确的做法是修改源数据 (如author.books = []),让计算属性自动更新。

六、课后 Quiz:巩固你的理解

问题1:计算属性和方法的核心区别是什么?

答案

计算属性基于依赖缓存 ,只有依赖变化时才重新计算;方法无缓存 ,每次调用都重新执行。
解析:缓存是计算属性的核心优势,适合需要重复使用且依赖稳定的逻辑(如过滤列表);方法适合不需要缓存的场景(如事件处理)。

问题2:如何创建一个可写的计算属性?

答案

通过同时定义get(读取逻辑)和set(修改逻辑)实现:

  • Options API:在computed选项中写{ get() {}, set(newValue) {} }
  • Composition API:调用computed({ get() {}, set(newValue) {} })
    示例 :参考"三、可写计算属性"中的fullName例子。

问题3:为什么计算属性中使用Date.now()不会更新?

答案

因为Date.now()不是响应式依赖 ,Vue无法追踪它的变化。计算属性只会在依赖的响应式数据变化时重新计算。
拓展 :如果需要实时获取时间,应该用setIntervalwatch,而不是计算属性。

七、常见报错及解决方案

报错1:计算属性返回undefined

原因getter函数没有返回值(忘记写return)。
示例

javascript 复制代码
const badComputed = computed(() => {
  author.books.length > 0 ? 'Yes' : 'No' // 忘记return
})

解决 :确保getter函数有return语句。

报错2:无法修改只读计算属性

报错信息Set operation on key "xxx" failed: computed value is readonly
原因 :尝试修改无setter 的计算属性(默认是只读的)。
解决

  • 如果需要修改,给计算属性添加setter(参考"三、可写计算属性");
  • 不要直接修改计算属性,改为修改源数据。

报错3:计算属性不随数据更新

原因

  1. 依赖了非响应式数据 (如普通变量,不是ref/reactive);
  2. getter函数中没有用到响应式数据(即无依赖)。
    解决
  • 将数据转为响应式(用refreactive包裹);
  • 确保getter函数中用到了响应式数据。

参考链接

vuejs.org/guide/essen...

相关推荐
wordbaby3 分钟前
TanStack Router 基于文件的路由
前端
wordbaby8 分钟前
TanStack Router 路由概念
前端
wordbaby10 分钟前
TanStack Router 路由匹配
前端
cc蒲公英11 分钟前
vue nextTick和setTimeout区别
前端·javascript·vue.js
程序员刘禹锡16 分钟前
Html中常用的块标签!!!12.16日
前端·html
我血条子呢26 分钟前
【CSS】类似渐变色弯曲border
前端·css
DanyHope27 分钟前
LeetCode 两数之和:从 O (n²) 到 O (n),空间换时间的经典实践
前端·javascript·算法·leetcode·职场和发展
hgz071028 分钟前
企业级多项目部署与Tomcat运维实战
前端·firefox
用户18878710698428 分钟前
基于vant3的搜索选择组件
前端
zhoumeina9928 分钟前
懒加载图片
前端·javascript·vue.js