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

Vue模板语法 计算属性在模板中的正确使用姿势

2022-08-134.0k 阅读

1. 计算属性基础

在Vue中,计算属性是一种非常强大的特性。它允许我们基于其他响应式数据进行复杂的计算,并将计算结果缓存起来。这意味着,只有当依赖的响应式数据发生变化时,计算属性才会重新求值。

假设有这样一个场景,我们有一个购物车的应用,需要实时计算购物车中商品的总价。如果不使用计算属性,我们可能会在模板中直接编写复杂的表达式来计算总价。

<div id="app">
  <ul>
    <li v-for="(item, index) in items" :key="index">
      {{ item.name }} - {{ item.price }} 元 x {{ item.quantity }} = {{ item.price * item.quantity }} 元
    </li>
  </ul>
  <p>总价: {{ items.reduce((acc, item) => acc + item.price * item.quantity, 0) }} 元</p>
</div>
<script>
  const app = new Vue({
    el: '#app',
    data() {
      return {
        items: [
          { name: '苹果', price: 5, quantity: 2 },
          { name: '香蕉', price: 3, quantity: 3 }
        ]
      }
    }
  });
</script>

虽然上述代码可以计算出总价,但如果在模板中多次需要用到总价,或者计算逻辑变得更加复杂,模板就会变得难以维护。这时,计算属性就派上用场了。

<div id="app">
  <ul>
    <li v-for="(item, index) in items" :key="index">
      {{ item.name }} - {{ item.price }} 元 x {{ item.quantity }} = {{ item.price * item.quantity }} 元
    </li>
  </ul>
  <p>总价: {{ totalPrice }} 元</p>
</div>
<script>
  const app = new Vue({
    el: '#app',
    data() {
      return {
        items: [
          { name: '苹果', price: 5, quantity: 2 },
          { name: '香蕉', price: 3, quantity: 3 }
        ]
      }
    },
    computed: {
      totalPrice() {
        return this.items.reduce((acc, item) => acc + item.price * item.quantity, 0);
      }
    }
  });
</script>

在上述代码中,我们在Vue实例的computed选项中定义了一个totalPrice计算属性。在模板中,我们直接使用totalPrice,而不需要每次都重复计算总价的逻辑。这样不仅使模板更简洁,而且计算属性会被缓存,只有当items数组发生变化时,totalPrice才会重新计算。

2. 计算属性的缓存机制

计算属性的缓存机制是其重要特性之一。为了更好地理解这一点,我们来看一个更复杂的例子。

假设我们有一个文章列表,每个文章都有一个发布时间。我们希望在模板中展示距离现在的时间差,例如“1小时前”、“2天前”等。

<div id="app">
  <ul>
    <li v-for="(article, index) in articles" :key="index">
      {{ article.title }} - {{ getTimeDiff(article.publishedAt) }}
    </li>
  </ul>
</div>
<script>
  function getTimeDiff(timestamp) {
    const now = new Date();
    const diff = now - new Date(timestamp);
    const minute = 1000 * 60;
    const hour = minute * 60;
    const day = hour * 24;
    if (diff < minute) {
      return Math.floor(diff / 1000) + '秒前';
    } else if (diff < hour) {
      return Math.floor(diff / minute) + '分钟前';
    } else if (diff < day) {
      return Math.floor(diff / hour) + '小时前';
    } else {
      return Math.floor(diff / day) + '天前';
    }
  }
  const app = new Vue({
    el: '#app',
    data() {
      return {
        articles: [
          { title: '第一篇文章', publishedAt: new Date('2023-10-01T10:00:00Z') },
          { title: '第二篇文章', publishedAt: new Date('2023-10-02T15:30:00Z') }
        ]
      }
    },
    methods: {
      getTimeDiff(timestamp) {
        return getTimeDiff(timestamp);
      }
    }
  });
</script>

在上述代码中,我们通过methods定义了一个getTimeDiff方法来计算时间差。在模板中,每次渲染文章列表时,都会调用getTimeDiff方法。如果文章列表很长,这个计算过程会比较耗时,即使文章的发布时间没有变化,也会重复计算。

接下来,我们使用计算属性来优化这个问题。

<div id="app">
  <ul>
    <li v-for="(article, index) in articles" :key="index">
      {{ article.title }} - {{ article.timeDiff }}
    </li>
  </ul>
</div>
<script>
  function getTimeDiff(timestamp) {
    const now = new Date();
    const diff = now - new Date(timestamp);
    const minute = 1000 * 60;
    const hour = minute * 60;
    const day = hour * 24;
    if (diff < minute) {
      return Math.floor(diff / 1000) + '秒前';
    } else if (diff < hour) {
      return Math.floor(diff / minute) + '分钟前';
    } else if (diff < day) {
      return Math.floor(diff / hour) + '小时前';
    } else {
      return Math.floor(diff / day) + '天前';
    }
  }
  const app = new Vue({
    el: '#app',
    data() {
      return {
        articles: [
          { title: '第一篇文章', publishedAt: new Date('2023-10-01T10:00:00Z') },
          { title: '第二篇文章', publishedAt: new Date('2023-10-02T15:30:00Z') }
        ]
      }
    },
    computed: {
      articleTimeDiffs() {
        return this.articles.map(article => {
          return {
           ...article,
            timeDiff: getTimeDiff(article.publishedAt)
          };
        });
      }
    },
    created() {
      this.articles = this.articleTimeDiffs;
    }
  });
</script>

在上述代码中,我们定义了一个articleTimeDiffs计算属性。在created钩子函数中,我们将articles替换为articleTimeDiffs计算后的结果。这样,只有当articles数组发生变化时,articleTimeDiffs才会重新计算,从而提高了性能。

3. 计算属性的依赖追踪

计算属性能够知道它依赖的响应式数据。当这些依赖数据发生变化时,计算属性会重新求值。

例如,我们有一个学生成绩管理的应用,需要计算学生的平均成绩,并根据平均成绩判断学生是否通过。

<div id="app">
  <input type="number" v-model="score1">
  <input type="number" v-model="score2">
  <input type="number" v-model="score3">
  <p>平均成绩: {{ averageScore }}</p>
  <p>是否通过: {{ isPassed }}</p>
</div>
<script>
  const app = new Vue({
    el: '#app',
    data() {
      return {
        score1: 0,
        score2: 0,
        score3: 0
      }
    },
    computed: {
      averageScore() {
        return (this.score1 + this.score2 + this.score3) / 3;
      },
      isPassed() {
        return this.averageScore >= 60;
      }
    }
  });
</script>

在上述代码中,averageScore计算属性依赖于score1score2score3。当这三个分数中的任何一个发生变化时,averageScore会重新计算。而isPassed计算属性又依赖于averageScore,所以当averageScore变化时,isPassed也会重新计算。

4. 计算属性的Setter和Getter

计算属性默认只有Getter,但我们也可以为计算属性定义Setter。

假设有一个需求,我们有一个全大写的用户名显示,同时也有一个输入框可以修改用户名。当我们在输入框中输入新的用户名时,显示的全大写用户名也会更新。

<div id="app">
  <input type="text" v-model="userName">
  <p>全大写用户名: {{ upperCaseUserName }}</p>
</div>
<script>
  const app = new Vue({
    el: '#app',
    data() {
      return {
        _userName: ''
      }
    },
    computed: {
      userName: {
        get() {
          return this._userName;
        },
        set(newValue) {
          this._userName = newValue;
        }
      },
      upperCaseUserName: {
        get() {
          return this.userName.toUpperCase();
        },
        set(newValue) {
          this.userName = newValue.toLowerCase();
        }
      }
    }
  });
</script>

在上述代码中,我们为userNameupperCaseUserName都定义了Getter和Setter。当我们修改userName时,upperCaseUserName会根据新的userName值重新计算。而当我们通过v - model绑定修改upperCaseUserName时,会调用其Setter,从而更新userName

5. 在模板中使用计算属性的注意事项

5.1 避免过度复杂的计算

虽然计算属性可以处理复杂的逻辑,但如果计算逻辑过于复杂,会使代码难以理解和维护。在这种情况下,可以考虑将部分逻辑封装到方法中,然后在计算属性中调用这些方法。

例如,我们有一个复杂的数学计算,涉及多个步骤和公式。

<div id="app">
  <p>计算结果: {{ complexCalculation }}</p>
</div>
<script>
  function step1(num) {
    return num * 2;
  }
  function step2(num) {
    return num + 5;
  }
  function step3(num) {
    return Math.sqrt(num);
  }
  const app = new Vue({
    el: '#app',
    data() {
      return {
        number: 10
      }
    },
    computed: {
      complexCalculation() {
        let result = step1(this.number);
        result = step2(result);
        return step3(result);
      }
    }
  });
</script>

通过将复杂的计算步骤封装到方法中,计算属性的逻辑更加清晰。

5.2 注意依赖关系

确保计算属性依赖的响应式数据是正确的。如果依赖关系错误,可能会导致计算属性不会在预期的情况下重新求值。

例如,我们有一个列表过滤的功能,根据输入框的值过滤列表。

<div id="app">
  <input type="text" v-model="filterText">
  <ul>
    <li v-for="(item, index) in filteredItems" :key="index">
      {{ item }}
    </li>
  </ul>
</div>
<script>
  const app = new Vue({
    el: '#app',
    data() {
      return {
        list: ['苹果', '香蕉', '橙子'],
        filterText: ''
      }
    },
    computed: {
      filteredItems() {
        return this.list.filter(item => item.includes(this.filterText));
      }
    }
  });
</script>

在上述代码中,filteredItems计算属性依赖于listfilterText。如果错误地将filterText放在了一个非响应式的变量中,或者在修改list时没有通过Vue的响应式机制,就会导致filteredItems不能正确更新。

5.3 结合Vue的生命周期钩子

有时候,我们可能需要在计算属性求值之前或之后执行一些操作。这时可以结合Vue的生命周期钩子。

例如,我们有一个计算属性用于获取用户的地理位置信息,并且在获取到地理位置后需要发送一个统计请求。

<div id="app">
  <p>地理位置: {{ location }}</p>
</div>
<script>
  const app = new Vue({
    el: '#app',
    data() {
      return {
        _location: null
      }
    },
    computed: {
      location() {
        if (navigator.geolocation) {
          navigator.geolocation.getCurrentPosition(position => {
            this._location = {
              latitude: position.coords.latitude,
              longitude: position.coords.longitude
            };
          });
        }
        return this._location;
      }
    },
    created() {
      this.$watch('location', newLocation => {
        if (newLocation) {
          // 发送统计请求
          console.log('发送地理位置统计请求', newLocation);
        }
      });
    }
  });
</script>

在上述代码中,我们在created钩子函数中使用$watch监听location计算属性的变化。当location有值时,发送统计请求。

6. 计算属性与方法的对比

6.1 缓存机制

方法在每次调用时都会执行,而计算属性会缓存结果,只有依赖数据变化时才会重新计算。

例如,我们有一个方法和一个计算属性都用于计算一个数的平方。

<div id="app">
  <input type="number" v-model="num">
  <p>方法计算结果: {{ squareMethod(num) }}</p>
  <p>计算属性结果: {{ squareComputed }}</p>
</div>
<script>
  const app = new Vue({
    el: '#app',
    data() {
      return {
        num: 0
      }
    },
    methods: {
      squareMethod(n) {
        console.log('方法被调用');
        return n * n;
      }
    },
    computed: {
      squareComputed() {
        console.log('计算属性被计算');
        return this.num * this.num;
      }
    }
  });
</script>

在模板中多次使用squareMethod(num),每次都会输出“方法被调用”,而squareComputed只有在num变化时才会输出“计算属性被计算”。

6.2 应用场景

方法适用于不需要缓存结果,每次调用都需要实时计算的场景,例如触发事件时执行一些临时性的操作。而计算属性适用于基于响应式数据进行缓存计算的场景,例如实时计算购物车总价等。

7. 计算属性在组件中的使用

在Vue组件中,计算属性同样非常有用。

假设我们有一个商品组件,需要根据商品的库存数量显示不同的状态。

<template>
  <div>
    <p>{{ product.name }}</p>
    <p>库存状态: {{ stockStatus }}</p>
  </div>
</template>
<script>
  export default {
    props: {
      product: {
        type: Object,
        required: true
      }
    },
    computed: {
      stockStatus() {
        if (this.product.stock === 0) {
          return '缺货';
        } else if (this.product.stock < 10) {
          return '库存不足';
        } else {
          return '有货';
        }
      }
    }
  };
</script>

在上述组件中,我们通过props接收一个product对象,然后使用计算属性stockStatus根据商品的库存数量计算库存状态,并在模板中显示。

8. 计算属性与Watch的配合使用

有时候,计算属性和Watch配合使用可以实现更复杂的功能。

例如,我们有一个搜索功能,当搜索框的值变化时,我们不仅要过滤列表,还要记录搜索历史。

<div id="app">
  <input type="text" v-model="searchText">
  <ul>
    <li v-for="(item, index) in filteredList" :key="index">
      {{ item }}
    </li>
  </ul>
  <p>搜索历史: {{ searchHistory }}</p>
</div>
<script>
  const app = new Vue({
    el: '#app',
    data() {
      return {
        list: ['苹果', '香蕉', '橙子'],
        searchText: '',
        searchHistory: []
      }
    },
    computed: {
      filteredList() {
        return this.list.filter(item => item.includes(this.searchText));
      }
    },
    watch: {
      searchText(newValue) {
        if (newValue) {
          this.searchHistory.push(newValue);
        }
      }
    }
  });
</script>

在上述代码中,计算属性filteredList根据searchText过滤列表。而watch监听searchText的变化,当searchText有值时,将其添加到搜索历史中。

9. 深入理解计算属性的原理

Vue的计算属性是基于依赖追踪实现的。当Vue实例创建时,会对计算属性进行初始化。在初始化过程中,会为计算属性创建一个Watcher实例。

这个Watcher实例会收集计算属性依赖的响应式数据的Dep实例。Dep是Vue内部用于依赖收集和派发更新的类。当依赖的响应式数据发生变化时,Dep会通知所有依赖它的Watcher,从而触发计算属性的重新求值。

例如,在前面计算平均成绩的例子中,averageScore计算属性的Watcher会收集score1score2score3的Dep。当score1发生变化时,score1的Dep会通知averageScore的Watcher,averageScore会重新计算。

10. 优化计算属性的性能

10.1 减少不必要的依赖

尽量确保计算属性只依赖于真正需要的响应式数据。如果计算属性依赖了过多不必要的数据,会导致计算属性在一些不需要重新计算的情况下也进行重新求值。

例如,我们有一个用户信息展示组件,同时有一个全局配置项用于控制是否显示广告。如果用户信息的计算属性依赖了这个广告配置项,即使广告配置项变化不影响用户信息的展示,计算属性也会重新计算。

<template>
  <div>
    <p>用户名: {{ user.name }}</p>
    <p>邮箱: {{ user.email }}</p>
  </div>
</template>
<script>
  export default {
    data() {
      return {
        user: {
          name: '张三',
          email: 'zhangsan@example.com'
        },
        showAd: false
      }
    },
    computed: {
      // 错误示例,不应该依赖showAd
      userInfo() {
        return {
         ...this.user,
          showAd: this.showAd
        };
      }
    }
  };
</script>

正确的做法是将不相关的依赖去除,只保留与用户信息相关的依赖。

10.2 合理使用缓存

利用计算属性的缓存机制,避免重复计算。如果计算属性的计算过程比较耗时,并且依赖的数据变化频率不高,那么计算属性的缓存可以显著提高性能。

例如,在处理大数据量的列表过滤时,使用计算属性进行过滤,并结合防抖或节流技术,可以进一步优化性能。

<div id="app">
  <input type="text" v-model="filterText">
  <ul>
    <li v-for="(item, index) in filteredList" :key="index">
      {{ item }}
    </li>
  </ul>
</div>
<script>
  import _ from 'lodash';
  const app = new Vue({
    el: '#app',
    data() {
      return {
        largeList: Array.from({ length: 10000 }, (_, i) => `item${i}`),
        filterText: ''
      }
    },
    computed: {
      filteredList() {
        return _.debounce(() => {
          return this.largeList.filter(item => item.includes(this.filterText));
        }, 300)();
      }
    }
  });
</script>

在上述代码中,我们使用lodashdebounce函数来延迟过滤操作,减少计算次数,结合计算属性的缓存,提高了性能。

通过以上对Vue模板语法中计算属性在模板中的正确使用姿势的详细介绍,相信大家对计算属性有了更深入的理解和掌握,可以在实际项目中更好地运用计算属性来优化代码和提升用户体验。