Vue 3计算属性的缓存与依赖追踪原理是什么?可写性与历史值功能该如何正确使用?

79 阅读15分钟

计算属性的基本用法

计算属性是Vue 3中用于派生响应式值的核心工具。当你需要根据现有响应式数据生成新值时,计算属性能让代码更简洁、更易维护。

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

假设我们有一个author对象,包含namebooks数组。我们需要在模板中显示“是否有已出版书籍”的结果:

<script setup>
import { reactive, computed } from 'vue'

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

// 计算属性:根据books长度派生结果
const publishedBooksMessage = computed(() => {
  // getter函数:返回派生值
  return author.books.length > 0 ? 'Yes' : 'No'
})
</script>

<template>
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span> <!-- 直接使用计算属性 -->
</template>

关键点

  • 计算属性通过computed()函数创建,接收一个getter函数(返回派生值)。
  • 模板中使用计算属性时,无需加.value(Vue会自动解包)。
  • 计算属性会自动追踪依赖(这里依赖author.books),当author.books变化时,publishedBooksMessage会自动更新。

缓存机制:为什么计算属性比方法更高效?

计算属性的核心优势是缓存——只有当依赖的响应式数据变化时,才会重新计算;否则直接返回缓存值。

缓存的原理

计算属性的缓存基于依赖的响应式数据

  1. 当计算属性的getter首次执行时,Vue会记录它访问的所有响应式数据(即“依赖”)。
  2. 之后,只有当这些依赖发生变化时,getter才会再次执行,更新缓存值。
  3. 如果依赖没有变化,多次访问计算属性会直接返回缓存值,避免重复计算。
往期文章归档
免费好用的热门在线工具

缓存的必要性:避免重复计算昂贵操作

假设你有一个需要遍历1000条数据的计算属性:

const expensiveComputed = computed(() => {
  return largeArray.value.filter(item => item.isActive).length
})

如果用方法实现(function calculateActiveCount() { ... }),每次组件渲染都会重新遍历1000条数据;而计算属性只会在largeArray变化时重新计算,大幅提升性能。

反例:非响应式依赖不会触发缓存更新

如果计算属性的getter访问了非响应式数据(如Date.now()),缓存永远不会更新:

// 这个计算属性永远不会变!因为Date.now()不是响应式依赖
const now = computed(() => Date.now())

依赖追踪:Vue如何“知道”计算属性依赖了什么?

Vue的响应式系统通过依赖收集实现计算属性的自动更新,流程如下(用流程图简化):

sequenceDiagram
    participant 计算属性 as Computed Prop
    participant 响应式数据 as Reactive Data
    participant Vue响应式系统 as Vue Reactive System

    Note over 计算属性: 首次执行getter
    计算属性->>响应式数据: 访问author.books
    Vue响应式系统->>计算属性: 记录依赖(计算属性依赖author.books)
    Note over 响应式数据: author.books.push("新书籍")
    响应式数据->>Vue响应式系统: 数据变化
    Vue响应式系统->>计算属性: 触发重新计算
    计算属性->>计算属性: 执行getter,更新缓存
    计算属性->>模板: 通知模板更新

详细解释

  • getter执行时,Vue会通过**代理(Proxy)**拦截响应式数据的访问(如author.books)。
  • Vue将当前计算属性标记为这些响应式数据的“依赖”(存入依赖列表)。
  • 当响应式数据变化时,Vue会遍历其依赖列表,触发所有相关计算属性重新计算。

计算属性 vs 方法:核心区别

特性计算属性方法
缓存有(依赖变化才更新)无(每次调用都执行)
适用场景派生响应式值(如过滤、排序)执行操作(如事件处理、异步请求)
模板中使用方式{{ computedProp }}{{ method() }}

代码对比

<script setup>
import { reactive } from 'vue'

const author = reactive({ books: ['Vue 3 Guide'] })

// 计算属性:缓存结果
const computedMessage = computed(() => author.books.length > 0 ? 'Yes' : 'No')

// 方法:无缓存
function methodMessage() {
  return author.books.length > 0 ? 'Yes' : 'No'
}
</script>

<template>
  <!-- 计算属性:依赖不变时直接返回缓存 -->
  <p>Computed: {{ computedMessage }}</p>
  <!-- 方法:每次渲染都重新执行 -->
  <p>Method: {{ methodMessage() }}</p>
</template>

可写计算属性:不止是“读”,还能“写”

默认情况下,计算属性是只读的(只有getter)。如果需要双向绑定计算属性(如通过表单修改),可以添加setter方法。

示例:双向绑定fullName

<script setup>
import { ref, computed } from 'vue'

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

// 可写计算属性:getter + setter
const fullName = computed({
  // getter:从firstName和lastName派生fullName
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  // setter:从fullName反向修改firstName和lastName
  set(newValue) {
    const [newFirst, newLast] = newValue.split(' ') // 分割新值
    firstName.value = newFirst || '' // 处理空值
    lastName.value = newLast || ''
  }
})
</script>

<template>
  <!-- 双向绑定计算属性 -->
  <input v-model="fullName" placeholder="Enter full name" />
  <p>First Name: {{ firstName }}</p> <!-- 自动更新 -->
  <p>Last Name: {{ lastName }}</p>  <!-- 自动更新 -->
</template>

使用场景:需要将计算属性作为“双向绑定的桥梁”(如表单输入、联动组件)。

获取计算属性的之前值(Vue 3.4+)

Vue 3.4+支持在计算属性的getter中获取之前的缓存值(通过previous参数),适用于需要“保留历史状态”的场景。

示例:限制值不超过3

<script setup>
import { ref, computed } from 'vue'

const count = ref(2)

// 只有count <=3时返回当前值,否则返回之前的值
const alwaysSmall = computed((previous) => {
  if (count.value <= 3) {
    return count.value
  }
  return previous // 返回之前的缓存值
})
</script>

<template>
  <button @click="count++">Increment Count ({{ count }})</button>
  <p>Always Small: {{ alwaysSmall }}</p> <!-- 当count=4时,显示3 -->
</template>

最佳实践:避免踩坑

  1. Getter必须无副作用: 计算属性的getter应该只返回派生值,不要做以下操作:

    • 修改其他响应式状态(如this.someValue = 'changed'
    • 发送网络请求(fetch('/api/data')
    • 操作DOM(document.querySelector('.foo').innerHTML = 'bar'
    • 异步操作(async函数)

    错误示例

    // 不要这么写!getter有副作用(修改其他状态)
    const badComputed = computed(() => {
      this.otherValue = 'changed' // 副作用
      return this.author.books.length > 0 ? 'Yes' : 'No'
    })
    
  2. 不要修改计算属性的返回值: 计算属性的返回值是“派生状态的快照”,修改它不会影响原始依赖,反而会导致状态不一致。

    // 错误:修改计算属性的返回值(无效)
    publishedBooksMessage.value = 'Maybe' // 不会改变author.books
    

课后Quiz:巩固所学

  1. 问题:计算属性和方法的核心区别是什么? 答案:计算属性基于依赖的响应式数据缓存结果,只有依赖变化时才重新计算;方法每次调用都会重新执行。

  2. 问题:什么时候需要使用可写计算属性? 答案:当需要双向绑定计算属性(如通过表单输入修改计算属性,同时反向修改其依赖的响应式数据)时。

  3. 问题:为什么计算属性的getter不能有副作用? 答案:计算属性的职责是“派生值”,副作用会导致不可预测的行为(如重复修改状态、多次发请求),破坏单向数据流原则。

常见报错及解决

1. 报错:Cannot assign to read only property 'value' of object '#<Object>'

原因:试图给默认的只读计算属性(只有getter)赋值。 解决:将计算属性改为可写,添加set方法:

// 错误:只读计算属性
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
fullName.value = 'Jane Smith' // 报错

// 正确:添加set方法
const fullName = computed({
  get() { /* ... */ },
  set(newValue) { /* ... */ }
})

2. 报错:Computed property 'xxx' was assigned to but it has no setter

原因:同1,试图给没有set方法的计算属性赋值。 解决:添加set方法,或检查是否误将计算属性当作普通响应式数据修改。

3. 报错:Computed property getter returned undefined

原因:计算属性的getter没有覆盖所有分支,导致返回undefined解决:确保getter在所有情况下都有返回值:

// 错误:缺少else分支
const publishedMessage = computed(() => {
  if (author.books.length > 0) {
    return 'Yes'
  }
  // 没有返回值,导致undefined
})

// 正确:覆盖所有分支
const publishedMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})

参考链接:vuejs.org/guide/essen…