Vue计算方法与侦听器

130 阅读5分钟

现在有一段代码,在Vue中定义了firstName和lastName,现在需要在屏幕上显示全名fullName

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <title>Document</title>
</head>
<body>
    <div id="app">hello</div>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                firstName: "Dell",
                lastName: "Smith"
            }
        })
    </script>
    
</body>
</html>

使用插值表达式

首先想到的应该是写一个插值表达式:

<body>
    <div id="app">{{firstName+" "+lastName}}</div>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                firstName: "Dell",
                lastName: "Smith"
            }
        })
    </script>
    
</body>

很容易就完成了。使用插值表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如:

<div id="example">
  {{ message.split('').reverse().join('') }}
</div>

在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量 message 的翻转字符串。当你想要在模板中多包含此处的翻转字符串时,就会更加难以处理。

使用方法

既然使用插值表达式不是一个好的选择。那可以选择使用方法来完成,也很简单。直接定义一个fullName函数,在插值表达式中调用它即可:

<body>
    <div id="app">{{fullName()}}</div>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                firstName: "Dell",
                lastName: "Smith"
            },
            methods: {
                fullName: function() {
                    return this.firstName+" "+this.lastName
                }
            }
        })
    </script> 
</body>

这样做很好,但是我们还有更优的方法。

计算属性

我们可以使用计算属性来解决上面的问题:

<body>
    <div id="app">{{fullName}}</div>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                firstName: "Dell",
                lastName: "Smith"
            },
            computed: {
                fullName: function() {
                    return firstName+" "+lastName
                }
            }
        })
    </script> 
</body>

从上面的写法可以看到,和方法对比,计算属性只是在computed里面定义,而方法是在methods中定义。在div中展示时,方法后面要加(),表示调用此方法,而计算属性不用。除这些外,再没有其他区别。 但是相比于方法,计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。比如我们可以在上面的代码中添加另一个属性age,并在app中展示它:

<body>
    <div id="app">{{fullName+" "+age}}</div>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                firstName: "Dell",
                lastName: "Smith",
                age: 30
            },
            computed: {
                fullName: function() {
                    return this.firstName+" "+this.lastName
                }
            }
        })
    </script>   
</body>

页面会展示出Dell Smith 30,如果现在将age更改,页面重新渲染时只会改变age的值,前面的fullName没有发生改变,多次访问 fullName计算属性会立即返回之前的计算结果,而不必再次执行函数。

<body>
    <div id="app">{{fullName()}}</div>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                firstName: "Dell",
                lastName: "Smith",
                age: 30
            },
            methods: {
                fullName: function() {
                    return this.firstName+" "+this.lastName+" "+this.age
                }
            }
        })
    </script> 
</body>

而方法则不会这样,它没有缓存可以依赖,每当触发重新渲染时,调用方法将总会再次执行函数。也就是age更新一次,整个fullName都要重新计算一次。

我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A。如果没有缓存,我们将不可避免的多次执行 A 的 getter!如果你不希望有缓存,请用方法来替代。

侦听器

除了计算属性,我们也可以用侦听器来完成:

<body>
    <div id="app">{{fullName}}</div>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                firstName: "Dell",
                lastName: "Smith",
                fullName: "Dell Smith"
            },
            watch: {
                firstName: function(){
                    this.fullName = this.firstName+" "+this.lastName
                },
                lastName: function(){
                    this.fullName = this.firstName+" "+this.lastName
                }
            }
        })
    </script> 
</body>

上面代码是命令式且重复的。将它与计算属性的版本进行比较,很明显计算属性更好。

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

<body>
    <div id="watch-example">
        <p>
            Ask a yes/no question:
            <input v-model="question">
        </p>
        <p>{{ answer }}</p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
  el: '#watch-example',
  data: {
    question: '',
    answer: 'I cannot give you an answer until you ask a question!'
  },
  watch: {
    // 如果 `question` 发生改变,这个函数就会运行
    question: function (newQuestion, oldQuestion) {
      this.answer = 'Waiting for you to stop typing...'
      this.debouncedGetAnswer()
    }
  },
  created: function () {
    // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
    // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
    // AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
    // `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
    // 请参考:https://lodash.com/docs#debounce
    this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
  },
  methods: {
    getAnswer: function () {
      if (this.question.indexOf('?') === -1) {
        this.answer = 'Questions usually contain a question mark. ;-)'
        return
      }
      this.answer = 'Thinking...'
      var vm = this
      axios.get('https://yesno.wtf/api')
        .then(function (response) {
          vm.answer = _.capitalize(response.data.answer)
        })
        .catch(function (error) {
          vm.answer = 'Error! Could not reach the API. ' + error
        })
    }
  }
})
</script>
</body>

在这个示例中,使用 watch 选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的.