跳转到内容

计算属性

基本示例

模板中的内联表达式非常方便,但它们主要用于简单的操作。将太多逻辑放入模板会使模板变得臃肿且难以维护。例如,如果我们有一个包含嵌套数组的对象

js
export default {
  data() {
    return {
      author: {
        name: 'John Doe',
        books: [
          'Vue 2 - Advanced Guide',
          'Vue 3 - Basic Guide',
          'Vue 4 - The Mystery'
        ]
      }
    }
  }
}
js
const author = reactive({
  name: 'John Doe',
  books: [
    'Vue 2 - Advanced Guide',
    'Vue 3 - Basic Guide',
    'Vue 4 - The Mystery'
  ]
})

并且我们想要根据作者是否已有书籍显示不同的消息

template
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>

此时,模板变得有些杂乱。我们需要花点时间才能意识到它根据 author.books 执行计算。更重要的是,如果我们需要多次将此计算包含在模板中,我们可能不想重复自己。

这就是为什么对于包含响应式数据的复杂逻辑,建议使用 计算属性。以下是相同的示例,经过重构

js
export default {
  data() {
    return {
      author: {
        name: 'John Doe',
        books: [
          'Vue 2 - Advanced Guide',
          'Vue 3 - Basic Guide',
          'Vue 4 - The Mystery'
        ]
      }
    }
  },
  computed: {
    // a computed getter
    publishedBooksMessage() {
      // `this` points to the component instance
      return this.author.books.length > 0 ? 'Yes' : 'No'
    }
  }
}
template
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>

在沙箱中尝试

在这里,我们声明了一个计算属性 publishedBooksMessage

尝试更改应用程序 databooks 数组的值,您将看到 publishedBooksMessage 如何相应地变化。

您可以在模板中将计算属性与数据绑定,就像绑定正常属性一样。Vue知道 this.publishedBooksMessage 依赖于 this.author.books,因此当 this.author.books 发生变化时,它会更新所有依赖于 this.publishedBooksMessage 的绑定。

参见: 计算属性的类型化

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'
  ]
})

// a computed ref
const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})
</script>

<template>
  <p>Has published books:</p>
  <span>{{ publishedBooksMessage }}</span>
</template>

在沙箱中尝试

这里我们声明了一个计算属性 publishedBooksMessagecomputed() 函数期望传入一个 getter 函数,返回值是一个 计算引用。类似于普通的引用,你可以通过 publishedBooksMessage.value 访问计算结果。计算引用在模板中也会自动展开,所以你可以在模板表达式中直接引用它们而不需要 .value

计算属性会自动跟踪它的响应式依赖。Vue 知道 publishedBooksMessage 的计算依赖于 author.books,因此当 author.books 发生变化时,它将更新任何依赖于 publishedBooksMessage 的绑定。

另请参阅:计算属性类型

计算属性缓存与方法的比较

你可能已经注意到,我们可以通过在表达式中调用一个方法来实现相同的结果

template
<p>{{ calculateBooksMessage() }}</p>
js
// in component
methods: {
  calculateBooksMessage() {
    return this.author.books.length > 0 ? 'Yes' : 'No'
  }
}
js
// in component
function calculateBooksMessage() {
  return author.books.length > 0 ? 'Yes' : 'No'
}

与计算属性不同,我们可以将相同的函数定义为方法。对于最终结果,这两种方法实际上是完全相同的。然而,区别在于 计算属性是基于它们的响应式依赖进行缓存的。 计算属性只有在某些响应式依赖发生变化时才会重新评估。这意味着只要 author.books 没有变化,多次访问 publishedBooksMessage 将立即返回之前计算的结果,而不需要再次运行 getter 函数。

这也意味着以下计算属性永远不会更新,因为 Date.now() 不是一个响应式依赖

js
computed: {
  now() {
    return Date.now()
  }
}
js
const now = computed(() => Date.now())

相比之下,方法调用将在每次重新渲染时始终运行函数。

为什么我们需要缓存?想象我们有一个昂贵的计算属性 list,它需要遍历一个巨大的数组并进行大量的计算。然后我们可能有其他依赖于 list 的计算属性。如果没有缓存,我们可能需要执行 list 的 getter 比必要的次数多得多!在你不想缓存的情况下,可以使用方法调用。

可写计算属性

计算属性默认是只读的。如果你尝试为计算属性分配新值,你将收到一个运行时警告。在极少数需要“可写”计算属性的情况下,你可以通过提供 getter 和 setter 来创建一个

js
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    fullName: {
      // getter
      get() {
        return this.firstName + ' ' + this.lastName
      },
      // setter
      set(newValue) {
        // Note: we are using destructuring assignment syntax here.
        [this.firstName, this.lastName] = newValue.split(' ')
      }
    }
  }
}

现在当运行 this.fullName = 'John Doe' 时,setter 将被调用,并且 this.firstNamethis.lastName 将相应地更新。

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

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

const fullName = computed({
  // getter
  get() {
    return firstName.value + ' ' + lastName.value
  },
  // setter
  set(newValue) {
    // Note: we are using destructuring assignment syntax here.
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})
</script>

现在当运行 fullName.value = 'John Doe' 时,setter 将被调用,并且 firstNamelastName 将相应地更新。

获取上一个值

  • 仅支持 3.4+

如果你需要,可以通过访问 getter 的第一个参数来获取计算属性返回的上一个值

js
export default {
  data() {
    return {
      count: 2
    }
  },
  computed: {
    // This computed will return the value of count when it's less or equal to 3.
    // When count is >=4, the last value that fulfilled our condition will be returned
    // instead until count is less or equal to 3
    alwaysSmall(previous) {
      if (this.count <= 3) {
        return this.count;
      }

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

const count = ref(2)

// This computed will return the value of count when it's less or equal to 3.
// When count is >=4, the last value that fulfilled our condition will be returned
// instead until count is less or equal to 3
const alwaysSmall = computed((previous) => {
  if (count.value <= 3) {
    return count.value;
  }

  return previous;
})
</script>

如果你正在使用可写计算

js
export default {
  data() {
    return {
      count: 2
    }
  },
  computed: {
    alwaysSmall: {
      get(previous) {
        if (this.count <= 3) {
          return this.count;
        }

        return previous;
      },
      set(newValue) {
        this.count = newValue * 2;
      }
    }
  }
}
vue
<script setup>
import { ref, computed } from 'vue'

const count = ref(2)

const alwaysSmall = computed({
  get(previous) {
    if (count.value <= 3) {
      return count.value;
    }

    return previous;
  },
  set(newValue) {
    count.value = newValue * 2;
  }
})
</script>

最佳实践

getter 应该无副作用

记住,计算属性函数应该只执行纯计算,并且不产生副作用非常重要。例如,不要修改其他状态、进行异步请求或在内联计算属性中修改DOM! 将计算属性视为基于其他值声明性地描述如何推导出一个值——其唯一责任应该是计算并返回该值。在指南的后面部分,我们将讨论如何使用观察者来执行对状态变化的响应。

避免修改计算值

计算属性返回的值是从衍生状态推导出来的。将其视为一个临时快照——每次源状态发生变化时,都会创建一个新的快照。修改快照是没有意义的,因此计算属性返回的值应被视为只读的,并且不得修改——相反,应该更新它所依赖的源状态来触发新的计算。

已加载计算属性