# 计算属性和侦听器

# 计算“属性”

# 缘由

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护

当你想要在模板中多次引用此处的翻转字符串时,就会更加难以处理。所以,对于任何复杂逻辑,你都应当使用计算属性

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Title</title>
  </head>

  <body>
    <div id="app">
      <!-- 在模板中放入太多的逻辑会让模板过重且难以维护 -->
      <h2>{{ message.split('').reverse().join('') }}</h2>
      <!-- 计算属性 -->
      <h2>{{ reversedMessage }}</h2>

      <!-- 在模板中放入太多的逻辑会让模板过重且难以维护 -->
      <h2>{{firstName +" "+ lastName}}</h2>
      <!-- 方法 -->
      <h2>{{fullNameMethod()}}</h2>
      <!-- 计算属性 -->
      <h2>{{fullNameComputed}}</h2>
    </div>

    <script src="/lib/vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          message: "Hello",
          firstName: "Steven",
          lastName: "Jobs",
        },
        // `this` 指向 vm 实例
        computed: {
          reversedMessage() {
            return this.message.split("").reverse().join("");// 字符串反转
          },
          fullNameComputed() {
            console.log("fullName computed");
            return this.firstName + " " + this.lastName;
          },
        },
        methods: {
          fullNameMethod() {
            console.log("fullName methods");
            return this.firstName + " " + this.lastName;
          },
        },
      });
    </script>
  </body>
</html>

可以像绑定普通属性一样在模板中绑定计算属性。Vue 知道 this.reversedMessage 依赖于 this.message,因此当 this.message 发生改变时,所有依赖 this.reversedMessage 的绑定也会更新。而且最妙的是我们已经以声明的方式创建了这种依赖关系:计算属性的 getter 函数是没有副作用 (side effect) 的,这使它更易于测试和理解。

如下例子,计算书本总价格,可以使用计算属性

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue</title>
  </head>

  <body>
    <div id="app">
      总价:{{totalPrice}}
    </div>

    <script src="/lib/vue.js"></script>
    <script>
      // vue.js文件中定义了 Vue 对象,使用时可以 new 构造出,并且它还有参数(对象类型)
      const vm = new Vue({
        el: "#app",
        // 声明式编程(声明式渲染),不再使用命令式
        data: {
          books: [
            { id: 1, name: "Java", price: 110 },
            { id: 2, name: "Kotlin", price: 120 },
            { id: 3, name: "Clojure", price: 112 },
            { id: 4, name: "JVM", price: 200 },
          ],
        },
        methods: {},
        computed: {
          // 匿名函数
          totalPrice() {
            let totalPrice = 0;
            this.books.forEach((item) => {
              totalPrice += item.price;
            });
            return totalPrice;
          },
          // 正常函数
          totalPrice2: function () {
            let totalPrice = 0;
            this.books.forEach((item) => {
              totalPrice += item.price;
            });
            return totalPrice;
          },
        },
      });
    </script>
  </body>
</html>

# 计算属性的 setter、getter

计算属性默认只有 getter ,不过在需要时你也可以提供一个 setter :

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Title</title>
  </head>

  <body>
    <div id="app">
      <!-- 计算属性 -->
      <h2>{{fullNameComputed}}</h2>
    </div>

    <script src="/lib/vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          firstName: "Steven",
          lastName: "Jobs",
        },
        computed: {
          // 1 简写
          //   fullNameComputed() {
          //     console.log("fullName computed");
          //     return this.firstName + " " + this.lastName;
          //   },

          // 2 实际上,一般计算属性无需写setter方法,再简化就成为上述简写形式了
          fullNameComputed: {
            get: function () {
              console.log("getter");
              return this.firstName + " " + this.lastName;
            },
            set: function (value) {
              console.log("setter", value);
              var [firstName, lastName] = value.split(" ");
              this.firstName = firstName;
              this.lastName = lastName;
            },
          },
        },
      });
    </script>
  </body>
</html>

运行 vm.fullName = 'John Doe' 时setter 会被调用,vm.firstNamevm.lastName 也会相应地被更新。

# 计算属性(缓存) VS 方法

计算属性是基于它们的响应式依赖进行缓存的只在相关响应式依赖发生改变时它们才会重新求值

如下例子,fullNameMethod 调用了3次,而 fullNameComputed 只调用了1次

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Title</title>
  </head>

  <body>
    <div id="app">
      <!-- 拼接 -->
      <h2>{{ firstName + " " + lastName}}</h2>
      <!-- 方法 -->
      <h2>{{fullNameMethod()}}</h2>
      <h2>{{fullNameMethod()}}</h2>
      <h2>{{fullNameMethod()}}</h2>

      <!-- 计算属性 -->
      <h2>{{fullNameComputed}}</h2>
      <h2>{{fullNameComputed}}</h2>
      <h2>{{fullNameComputed}}</h2>
    </div>

    <script src="/lib/vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          firstName: "Steven",
          lastName: "Jobs",
        },
        computed: {
          // 1 简写
          fullNameComputed() {
            console.log("fullName computed");
            return this.firstName + " " + this.lastName;
          },
        },
        methods: {
          fullNameMethod() {
            console.log("fullName methods");
            return this.firstName + " " + this.lastName;
          },
        },
      });
    </script>
  </body>
</html>

这也同样意味着下面的计算属性将不再更新,因为 Date.now() 不是响应式依赖:

computed: {
  now: function () {
    return Date.now()
  }
}

# 计算属性 vs 侦听属性 ❎

Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 watch——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式watch 回调。细想一下这个例子:

<div id="demo">{{ fullName }}</div>
var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
})

上面代码是命令式且重复的。将它与计算属性的版本进行比较:

var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

显而易见好得多了

# 侦听器 ❎

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。