MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue计算属性 简化复杂逻辑处理的最佳实践

2023-11-221.3k 阅读

Vue 计算属性基础

什么是计算属性

在 Vue 应用中,计算属性是一种基于响应式依赖进行缓存的属性。它们的值不是直接声明,而是通过一个函数计算得出。与普通的 methods 方法不同,计算属性会在依赖的数据发生变化时才重新计算,而 methods 每次调用都会执行函数。

例如,我们有一个简单的 Vue 实例,用于展示一个人的全名:

<template>
  <div>
    <p>First Name: <input v-model="firstName"></p>
    <p>Last Name: <input v-model="lastName"></p>
    <p>Full Name: {{ fullName }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    fullName() {
      return this.firstName + ' ' + this.lastName
    }
  }
}
</script>

在上述代码中,fullName 是一个计算属性。它依赖于 firstNamelastName。当 firstNamelastName 发生变化时,fullName 会重新计算。

计算属性的缓存机制

计算属性之所以高效,是因为它具有缓存机制。只有在它的依赖响应式数据发生变化时才会重新计算。这意味着如果依赖数据没有变化,多次访问计算属性会直接返回缓存的结果,而不会重复执行计算函数。

继续以上面的 fullName 计算属性为例,如果在模板中有多个地方使用 fullName

<template>
  <div>
    <p>First Name: <input v-model="firstName"></p>
    <p>Last Name: <input v-model="lastName"></p>
    <p>Full Name 1: {{ fullName }}</p>
    <p>Full Name 2: {{ fullName }}</p>
    <p>Full Name 3: {{ fullName }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    fullName() {
      console.log('Calculating fullName...');
      return this.firstName + ' ' + this.lastName
    }
  }
}
</script>

当页面首次渲染时,fullName 会被计算一次,控制台会输出 Calculating fullName...。之后,无论模板中有多少处使用 fullName,只要 firstNamelastName 不发生变化,fullName 不会再次计算,也就不会再次输出 Calculating fullName...

计算属性 vs 方法

虽然计算属性和 methods 方法都可以用来执行一些逻辑并返回结果,但它们有着本质的区别。

以之前的例子,如果我们使用 methods 来实现同样的功能:

<template>
  <div>
    <p>First Name: <input v-model="firstName"></p>
    <p>Last Name: <input v-model="lastName"></p>
    <p>Full Name: {{ getFullName() }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  methods: {
    getFullName() {
      return this.firstName + ' ' + this.lastName
    }
  }
}
</script>

在这个例子中,每次模板重新渲染时,getFullName 方法都会被调用,即使 firstNamelastName 没有变化。而计算属性只有在依赖数据变化时才会重新计算,这在性能上有着显著的优势,尤其是在复杂计算的情况下。

计算属性处理复杂逻辑

复杂数据处理

在实际项目中,我们经常需要处理复杂的数据。例如,有一个电商应用,我们有一个商品列表,每个商品对象包含价格、库存等信息。我们需要计算所有商品的总价以及库存总量。

假设商品列表数据如下:

const products = [
  { id: 1, name: 'Product 1', price: 10, stock: 5 },
  { id: 2, name: 'Product 2', price: 20, stock: 3 },
  { id: 3, name: 'Product 3', price: 15, stock: 7 }
]

我们可以在 Vue 组件中使用计算属性来处理:

<template>
  <div>
    <ul>
      <li v-for="product in products" :key="product.id">
        {{ product.name }} - Price: {{ product.price }}, Stock: {{ product.stock }}
      </li>
    </ul>
    <p>Total Price: {{ totalPrice }}</p>
    <p>Total Stock: {{ totalStock }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      products: [
        { id: 1, name: 'Product 1', price: 10, stock: 5 },
        { id: 2, name: 'Product 2', price: 20, stock: 3 },
        { id: 3, name: 'Product 3', price: 15, stock: 7 }
      ]
    }
  },
  computed: {
    totalPrice() {
      return this.products.reduce((acc, product) => acc + product.price, 0)
    },
    totalStock() {
      return this.products.reduce((acc, product) => acc + product.stock, 0)
    }
  }
}
</script>

在上述代码中,totalPricetotalStock 计算属性通过 reduce 方法对 products 数组进行复杂的数据计算。如果使用 methods 方法,每次渲染模板时都会重复执行这些计算,而计算属性只有在 products 数组发生变化时才会重新计算。

数据过滤与筛选

另一种常见的复杂逻辑是数据过滤与筛选。比如在一个博客应用中,我们有一个文章列表,每篇文章有标题、发布日期、分类等信息。我们希望根据用户选择的分类来过滤显示文章。

假设文章列表数据如下:

const posts = [
  { id: 1, title: 'Post 1', category: 'Tech', date: '2023-01-01' },
  { id: 2, title: 'Post 2', category: 'Life', date: '2023-02-01' },
  { id: 3, title: 'Post 3', category: 'Tech', date: '2023-03-01' }
]

在 Vue 组件中实现如下:

<template>
  <div>
    <select v-model="selectedCategory">
      <option value="">All</option>
      <option value="Tech">Tech</option>
      <option value="Life">Life</option>
    </select>
    <ul>
      <li v-for="post in filteredPosts" :key="post.id">
        {{ post.title }} - Category: {{ post.category }}, Date: {{ post.date }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      posts: [
        { id: 1, title: 'Post 1', category: 'Tech', date: '2023-01-01' },
        { id: 2, title: 'Post 2', category: 'Life', date: '2023-02-01' },
        { id: 3, title: 'Post 3', category: 'Tech', date: '2023-03-01' }
      ],
      selectedCategory: ''
    }
  },
  computed: {
    filteredPosts() {
      if (!this.selectedCategory) {
        return this.posts
      }
      return this.posts.filter(post => post.category === this.selectedCategory)
    }
  }
}
</script>

这里的 filteredPosts 计算属性根据 selectedCategory 的值来过滤 posts 数组。当 selectedCategory 发生变化时,filteredPosts 会重新计算,从而在模板中显示正确的过滤后文章列表。

嵌套数据处理

在处理嵌套数据结构时,计算属性同样能发挥重要作用。例如,有一个组织结构数据,每个部门有员工列表,我们需要计算每个部门的员工总数。

假设组织结构数据如下:

const organization = [
  {
    department: 'HR',
    employees: [
      { name: 'Alice', age: 25 },
      { name: 'Bob', age: 28 }
    ]
  },
  {
    department: 'Engineering',
    employees: [
      { name: 'Charlie', age: 30 },
      { name: 'David', age: 32 }
    ]
  }
]

在 Vue 组件中可以这样实现:

<template>
  <div>
    <ul>
      <li v-for="dept in organization" :key="dept.department">
        {{ dept.department }} - Employee Count: {{ getEmployeeCount(dept) }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      organization: [
        {
          department: 'HR',
          employees: [
            { name: 'Alice', age: 25 },
            { name: 'Bob', age: 28 }
          ]
        },
        {
          department: 'Engineering',
          employees: [
            { name: 'Charlie', age: 30 },
            { name: 'David', age: 32 }
          ]
        }
      ]
    }
  },
  computed: {
    getEmployeeCount() {
      return dept => dept.employees.length
    }
  }
}
</script>

这里的 getEmployeeCount 计算属性返回一个函数,该函数接收一个部门对象并返回该部门的员工数量。由于计算属性的缓存机制,当 organization 数据结构没有变化时,getEmployeeCount 不会重复计算每个部门的员工数量。

计算属性的高级应用

计算属性的 setter

计算属性默认只有 getter 方法,但在某些情况下,我们可能需要设置计算属性的值,这时就可以使用 setter 方法。例如,我们有一个包含年、月、日的日期对象,我们希望通过一个计算属性来设置完整的日期字符串,并且在设置这个字符串时能自动解析出年、月、日。

<template>
  <div>
    <p>Year: <input v-model="year"></p>
    <p>Month: <input v-model="month"></p>
    <p>Day: <input v-model="day"></p>
    <p>Full Date: <input v-model="fullDate"></p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      year: '',
      month: '',
      day: ''
    }
  },
  computed: {
    fullDate: {
      get() {
        return `${this.year}-${this.month}-${this.day}`
      },
      set(value) {
        const parts = value.split('-')
        this.year = parts[0]
        this.month = parts[1]
        this.day = parts[2]
      }
    }
  }
}
</script>

在上述代码中,fullDate 计算属性既有 getter 方法用于获取日期字符串,也有 setter 方法用于在设置日期字符串时更新 yearmonthday

基于多个数据源的计算属性

有时候,计算属性需要依赖多个不同的数据源。例如,在一个财务应用中,我们有收入列表和支出列表,我们需要计算净收入(收入总和减去支出总和)。

假设收入和支出列表数据如下:

const incomes = [100, 200, 150]
const expenses = [50, 75, 100]

在 Vue 组件中实现如下:

<template>
  <div>
    <ul>
      <li v-for="income in incomes" :key="income">Income: {{ income }}</li>
    </ul>
    <ul>
      <li v-for="expense in expenses" :key="expense">Expense: {{ expense }}</li>
    </ul>
    <p>Net Income: {{ netIncome }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      incomes: [100, 200, 150],
      expenses: [50, 75, 100]
    }
  },
  computed: {
    netIncome() {
      const totalIncome = this.incomes.reduce((acc, income) => acc + income, 0)
      const totalExpense = this.expenses.reduce((acc, expense) => acc + expense, 0)
      return totalIncome - totalExpense
    }
  }
}
</script>

这里的 netIncome 计算属性依赖于 incomesexpenses 两个数据源。当 incomesexpenses 中的任何一个发生变化时,netIncome 都会重新计算。

计算属性与 watcher 的结合使用

虽然计算属性本身已经能够处理很多响应式逻辑,但在某些复杂场景下,结合 watcher 可以实现更细致的控制。例如,我们有一个搜索框,用于搜索商品列表,同时我们希望在搜索结果为空时显示提示信息。

假设商品列表数据如下:

const products = [
  { id: 1, name: 'Product 1' },
  { id: 2, name: 'Product 2' },
  { id: 3, name: 'Product 3' }
]

在 Vue 组件中实现如下:

<template>
  <div>
    <input v-model="searchTerm" placeholder="Search products">
    <ul>
      <li v-for="product in filteredProducts" :key="product.id">
        {{ product.name }}
      </li>
    </ul>
    <p v-if="filteredProducts.length === 0 && searchTerm">No products found</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      products: [
        { id: 1, name: 'Product 1' },
        { id: 2, name: 'Product 2' },
        { id: 3, name: 'Product 3' }
      ],
      searchTerm: ''
    }
  },
  computed: {
    filteredProducts() {
      return this.products.filter(product => product.name.includes(this.searchTerm))
    }
  },
  watch: {
    searchTerm(newValue) {
      if (newValue === '') {
        // 清空搜索框时的额外逻辑
      }
    }
  }
}
</script>

在这个例子中,filteredProducts 计算属性用于过滤商品列表。同时,通过 watcher 监听 searchTerm 的变化,在搜索框清空时可以执行额外的逻辑,如重置一些状态等。这样计算属性和 watcher 相互配合,实现了更复杂的业务逻辑。

计算属性在项目中的优化与注意事项

优化计算属性性能

  1. 减少不必要的依赖:确保计算属性只依赖真正需要的数据。如果计算属性依赖了过多无关的数据,会导致在这些无关数据变化时也重新计算,影响性能。例如,在一个展示用户信息的组件中,如果计算属性 userFullInfo 只需要 user.firstNameuser.lastName,就不要让它依赖 user.address 等无关数据。
  2. 避免复杂计算嵌套:尽量避免在计算属性中进行非常复杂且嵌套多层的计算。如果计算逻辑过于复杂,可以考虑将其拆分成多个简单的计算属性或者辅助函数。例如,在计算一个复杂的报表数据时,可以先通过多个计算属性分别计算各个部分的数据,最后再组合这些部分得到最终结果。

注意响应式数据的更新

  1. 数组变异方法:当计算属性依赖数组时,使用数组的变异方法(如 pushpopsplice 等)会触发计算属性重新计算,因为这些方法会改变数组本身,从而触发 Vue 的响应式系统。但如果直接通过索引修改数组元素,如 arr[0] = 'new value',不会触发计算属性重新计算。在这种情况下,可以使用 Vue.set 方法来确保响应式更新。例如:
export default {
  data() {
    return {
      items: [1, 2, 3]
    }
  },
  computed: {
    sumOfItems() {
      return this.items.reduce((acc, item) => acc + item, 0)
    }
  },
  methods: {
    updateItem() {
      // 正确方式,会触发 sumOfItems 重新计算
      this.$set(this.items, 0, 10)
      // 错误方式,不会触发 sumOfItems 重新计算
      // this.items[0] = 10
    }
  }
}
  1. 对象属性更新:类似地,对于对象,如果直接添加或修改对象的属性,可能不会触发计算属性重新计算。例如:
export default {
  data() {
    return {
      user: { name: 'John' }
    }
  },
  computed: {
    greeting() {
      return 'Hello, ' + this.user.name
    }
  },
  methods: {
    updateUser() {
      // 正确方式,会触发 greeting 重新计算
      this.$set(this.user, 'age', 30)
      // 错误方式,不会触发 greeting 重新计算
      // this.user.age = 30
    }
  }
}

计算属性的可维护性

  1. 命名规范:给计算属性起一个清晰、有意义的名字。例如,在一个电商购物车组件中,计算购物车总价的计算属性命名为 cartTotalPrice 就比命名为 total 更清晰易懂,方便团队成员理解和维护代码。
  2. 逻辑分离:如果一个组件中有多个计算属性,尽量将相关的计算属性放在一起,并按照功能进行分组。例如,在一个博客管理组件中,可以将与文章列表展示相关的计算属性放在一组,与文章分类统计相关的计算属性放在另一组,这样代码结构更清晰,易于维护。

通过合理地使用计算属性,我们可以简化前端开发中的复杂逻辑处理,提高代码的性能和可维护性。在实际项目中,要根据具体的业务需求和场景,灵活运用计算属性的各种特性,以达到最佳的开发效果。