Vue2.0学习--基础--3--计算属性和侦听器

(day4)

前面三天学习了一些Vue核心的一些非常基本的功能:

  • 声明式渲染、条件和循环、处理用户输入、组件化应用构建
  • Vue实例:创建一个vue实例、数据与方法、实例生命周期钩子、生命周期图示
  • 模板语法:插值、指令、缩写

前面讲到的知识让我们开始认识到了Vue的模板语法,在第二节的时候我们知道了每一个Vue实例都有一个参数——选项对象,选项对象里面有许多选项(选项/数据、选项/DOM、选项/生命周期钩子、选项/资源、选项/组合、选项/其他),我们通过选用这些选项在Vue中完成我们想要完成的事情。

这一章要讲的计算属性,就是选项对象中的选项/数据包含data、props、propsData、computed、methods、watch)中的computed选项,在学习这个属性的过程中,我们还将顺便通过计算属性computed与方法methods的比较、与数据侦听watch的比较,来额外学习一些methods和watch的特性。

本章内容:计算属性(基础例子、与methods比较、与watch比较、计算属性的setter)、侦听器。

一、计算属性

 基础例子、与methods比较、与watch比较、计算属性的setter

计算属性就是Vue实例选项对象中选项/数据分类下的computed选项。

具体在什么地方使用,以及如何使用,下面会有详解。

这里举一个非常基础的例子:

在前面三天的学习中我们知道了Vue中的模板语法,我们知道在Mustache语法的双大括号中可以放简单的字符串或者单个JavaScript表达式。

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

<div>
    {{ message.split('').reverse().join('') }}
</div>

 在这个地方模板不再是简单的声明逻辑。当你在模板多处包含这个翻转字符串时,就会更加难以处理。

所以,对于任何复杂逻辑,你都应该使用计算属性

1、基础例子

<div id="example">
    <p>Original message:"{{ message }}"</p>
    <p>Computed reversed message: "{{ reverseMessage }}"</p>
</div>
var  vm = new Vue({
    el:'#example',
    data:{
        message:'hello'
    },
    computed:{
        //计算属性的getter
        reverseMessage:function(){
            //'this'指向vm实例
            return this.message.split('').reverse().join('')
        }
    }
});

(这里可以注意到我们的Mustache语法中的关键词和Vue选项对象中的computed选项中的reverseMessage属性相匹配了,前面我们基本上都是和data选中的属性名相匹配)

这里我们也要注意到computed对象中的reverseMessage方法里面调用的this值是指向vm实例的,而不是computed对象。

上面的结果为:

Original message:"hello"

Computed reversed message:"olleh"

 这里可以通过vm.message来改变message的值:

vm.message = "goodbye"

console.log(vm.reverseMessage);        //"eybdoog"

(其实这里我有些不解的是——为什么不是vm.data.message,而是vm直接调用了data属性(data属性也是一个对象字面量)上的message属性,即vm.message。这个留着以后来解决)

2、计算属性缓存 vs 方法

可能一些爱思考的同学就会发现:这里我们所做的就是没有将message.split("").reverse().join("")这个单句的JavaScript表达式直接写到Mustache语法的双大括号中,

为什么要这样写呢,是为了减轻{{}}中的复杂度。

那么如果单单是为了减轻{{}}中的复杂度,这和我们在methods选项里面定义reverseMessage有啥区别呢?这不是一样的吗?为啥还要单独搞这么一个computed选项。

 就像这样,在methods选中定义reverseMessage方法也是可以的啊:

//用这个methods选项代替前面的computed选项
methods:{
    reverseMessage:function(){
        return this.message.split("").reverse().join("");
    }
}

答案是肯定的,是能得到完全相同的结果。

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

(这里这样说,我也是不知道计算属性的响应式依赖具体是什么,以及它们发生变化导致计算属性重新加载的具体情况是什么,留着以后填坑。这里大致可以理解为计算属性,计算运行完成以后,就会将结果存储起来,除非与它有关的依赖发生改变,它的结果将会存储在那里不会发生变化)

在上面这个例子中,如果message没有发生变化,那么reverseMessage计算属性的就会立即返回之前的计算的结果,不会再执行该函数。

然而,每次触发重新渲染时,如果是调用方法(methods)将总会重新执行该函数。

(就是说,如果message的值没有发生改变,每次重新读取reverseMesaage的值,如果这个reverseMessage函数是computed选项里面的,那么就不用重算,直接去取出第一次计算出的结果就行,而如果reverseMessage函数是写在methods选项中的时候,每一次重新读取reverseMessage的值时,都会重新运行函数,然后获取计算出相应结果)

 假设我们有一个性能开销很大的属性A(就像是message),很多计算属性依赖这个A(就像是reverseMessage依赖于message),如果没有缓存,那么就会多次读取A,就会多次进行性能开销很大的行为,,,这种情况下,将reverseMessage函数写到methods选项就不是很明智了。

(这里有点像是惰性加载函数的思想,但Vue源码里面关于computed这一块是不是这种思路我就不清楚了,或许更高级,这个坑留着后面填)

#methods选项

 methods将被混入到Vue实例中,可以直接通过VM实例访问到这些方法,或者在指令表达式中使用。方法中的 this 自动绑定为Vue实例。

注意:不应该使用箭头函数来定义method函数。例如:

plus:() => this,a++;

在ES6中我们知道箭头函数中的this值是自动绑定了父级作用域的上下文。所以箭头函数中的this值不会按照期望指向Vue实例。

3、计算属性 vs 侦听属性

侦听属性:Vue提供的一种更通用的方式来观察和响应Vue实例上数据的变化。

#watch选项

  • 类型: { [key: string]: string | Function | Object | Array } 
  • 详细:一个对象,
    • 键是需要观察的表达式,值是对应回调函数。
    • 值也可以是方法名,或者包含选项的对象。
    • Vue实例将会在实例化时调用 $watch() ,遍历watch对象的每一个属性。
  • 示例:
    var vm = new Vue({
        data:{
            a:1,
            b:2,
            c:3,
            d:4,
            e:{
                f:{
                    g:5
                }
            }
        },
    
        watch:{
            a:function(val,oldVal){
                consolo.log('new:%s,old:%d',val,oldVal);
            },
    
            //方法名
            b:'someMethod',
    
            //该回调函数在任何被侦听的对象的property改变时被调用,不论其被嵌套多深
            c:{
                handler:function(val,oldVal){/*...*/},
                deep:true
            },
    
            //该回调函数将会在侦听开始之后被立即调用
            d:{
                handler:'someMethod',
                immediate:true
            },
    
            //你可以传入回调数组,他们会被逐一调用
            e:[
                'handler1',
                function handler2(cal,oldVal){/*...*/},
    
                {
                    handler:function handler3(val,oldVal){/*...*/}
                    /*...*/
                },
            ],
    
            //watch vm.e.f 的 value:{g:5}
            'e.f':function(val,oldVal){/*...*/}
        }
    });
    
    vm.a = 2;    //new;2, old:1
  • 注意:不能够使用箭头函数来定义watch函数。和上面methods选中讲的一样,this指向的问题。

 讲了这么多,我们是知道了通过watch选项侦听数据的变化,如果数据发生了变化,我们可以做出相应的“反应”,那么这个代表“反应”的函数就定义在watch选项中。
这里的函数可以是直接定义的function(){};也可以是一个函数名的引用'someMethod';也可以是引用对象字面量在对象字面量中放入我们的“反应”函数,和属性(deep、immediate);也可以是一个数组,在数组中放入我们需要依次调用的“反应”函数;也可以通过点运算符来细微的定义数据中的数据(如e中的f)发生变化时,调用的“反应”函数。

 这里讲到了如何调用watch选项,然后在里面放置我们想要观测的数据,及数据发生变化时采取的“反应”——函数的定义,十分的简单。接下来就要具体的去理解一下这个Vue的实例方法/数据-vm.$watch

#vm.$watch(expOrFn,callback,[options]):

  • 参数
    •  (string | Function) expOrFn 
    •  (Function | Object) callback 
    •  (Object) [options] 
      •  (boolean) deep 
      •  (boolean) immediate 
  • 返回值 (Function) unwatch 
  • 用法
    观察Vue实例上一个表达式或者是函数计算结果的变化。
    回调函数接受的参数是新值和旧值。
    表达式只接受简单的键路径。
    对于更复杂的表达式,用一个函数取代。
    • 注意:在变更(不是替换)对象或数组时,旧值与新值相同,因为它们指向同一个对象/数组。Vue不会保留变更之前的副本。(?)
  • 示例
    //$watch的第一个参数,可以是一个字符串表达式,也可以是一个函数
    
    //键路径(表达式只接受简单的键路径)
    vm.$watch('a.b.c',function(val,oldVal){
        //做点什么
    })
    
    //函数
    vm.$watch(
        function(){
            //表达式 'this.a + this.b' 每次得出一个不同的结果时,处理函数都会被调用
            //这就像监听一个未被定义的计算属性
            return this.a + this.b;
        },
        function(newVal,oldVal){
            //做点什么
        }
    )
    

     vm.$watch 返回一个取消观察函数,用来停止触发回调:

    var unwatch = vm.$watch('a',cb);
    //之后取消观察
    unwatch()

    (这里有些有些疑惑的就是,前面当this.a+this.b的值发生变化的时候,我们就回调函数,这种情况为什么就像是在监听计算属性)

  • 选项:deep
    为了 发现对象内部值的变化 ,可以在选项参数中指定 deep:true
    注意监听数组的变更不需要这么做。

    vm.$watch('someObject',callback,{
        deep:true
    });
    
    vm.someObject.nestedValue = 234
    //callback is fired
  • 选项:immediate
    在选项参数中指定 immediate:true 立即以表达式的当前值触发回调

    vm.$watch('a',callback,{
        immediate:true
    });
    //立即以'a'的当前值触发回调

    注意:在带有 immediate 选项时,你不能在第一次回调时取消侦听给定的property

    //这会导致报错
    var unwatch = vm.$watch(
        'value',
        
        function(){
            doSomething()
            unwatch()
        },
        
        {immediate:true}
    );

    在这里,因为immediate选项为true,所以我们会立即以表达式的当前值——‘value’触发回调——function(){},但是在这个回调里面调用了unwatch(),也就是终止侦听(停止触发回调)。
    但是,当我们的$watch()带有immediate选项时,不能在第一次侦听时取消给定的property。但是上面在侦听并回调函数时调用了取消侦听函数unwatch(),所以这里会触发错误。

        如果一定要在回调内部调用一个取消侦听的函数,应该先检查其函数的可用性:

var unwatch = vm.$watch(
    'value',
    
    function(){
        doSomething(){}
        if(unwatch){
            unwatch()
        }
    },

    {immediate:true}
);

(这里留一个坑,就是这个immediate为什么会有这样的特性,是怎么做到的)

 这就是关于Vue提供的侦听属性。上面讲到了选项/数据中的watch,以及实例方法/数据中的$watch(),那他们之间的关系与区别是什么呢?

在第二天的Vue实例中讲到过:Vue实例暴露了一些有用的实例property与方法,它们都有$前缀。
这个$前缀,使用来与用户定义的property区分开来。

(那这两种watch有什么区别,分别在什么时候用,埋一个坑)

回到这一节的主题——计算属性和侦听属性的比较。

如果当我们的一些数据随着其他数据变动而变动,很容易滥用 watch ,这样不好(为什么不好?)
通常,更好做法是使用计算属性而不是命令式的 watch 回调

<div id="demo"> {{fullName}} </div>
var vm = new Vue({
    el:'#demo',
    
    data:{
        firstName:'Douglas',
        lastName:'Crockford',
        fullName:'Douglas Crockford'
    },

    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:'Douglas',
        lastName:'Crockford'
    },

    computed:{
        fullName:function(){
            return this.firstName + ' ' + this.lastName;
        }
    }
});

这里我们直接将fullName属性定义为一个computed选项中的属性,那么fullName会在第一次计算出值以后缓存下来,这里的firstName与lastName就是fullName的依赖,当firstName或者lastName发生变化时,fullName函数才会再调用然后求值。

这里确实没有滥用watch,并且watch为了侦听firstName和lastName两个数据的变化,设定了两个回调函数,而计算属性只定义了一个函数。

但是为什么计算属性能够检测到firstName以及lastName——这两个依赖的变化呢?在这后面又是什么完成了这个侦听依赖变化。这些都是之后需要弄明白的的地方。

4、计算属性的setter

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

//...
computed:{
    fullName:{
        //getter
        get:function(){
            return this.firstName + ' ' + this.lastName;
        },

        //setter
        set:function(newValue){
            var names = newValue.split(' ');
            this.firstName = names[0];
            this.lastName = names[names.length-1];
        }
    }
}

//现在运行 vm.fullName = 'ding ding'setter会被调用
//firstName 和 lastName也会相应的被更新
vm.fullName = 'ding ding dang';

 5、侦听器

 通过上面计算属性和侦听器的比较,我们可能会觉得,既然是这样的,那么我们所有的使用watch的情况都可以用计算属性来代替啊。

并不是这样的。

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。

(这就是为什么Vue通过watch选项提供了一个更通用的方法,来响应数据的变化。数据变化时执行异步或者开销较大的操作时,这个方式是最有用的。)

<div id="watch-example">
  <p>
    Ask a yes/no question:
    <input v-model="question">
  </p>
  <p>{{ answer }}</p>
</div>
<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 -->
<!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->
<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>

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

也就是说,计算属性有它的一些优点,但是如果我们需要在数据变化的过程中执行操作,也就是计算属性computed选项中定义的属性的依赖发生变化的过程中,需要调用“反应”方法,计算属性是做不到的。

也许这样说也许太绕了,我们先看一下前面为什么计算属性能够代替watch来侦听数据的变化:因为计算属性的结果是根据依赖响应式缓存的。这里watch中侦听的数据变化,可以看做是我们计算属性中依赖的变化。watch侦听到数据变化,就要回调函数进行相应反应,这就像是在计算属性中,依赖发生变化,我们的计算属性中相应函数就会执行然后得到新的结果。

对于watch前面说到的情况,当要在数据变化过程中执行异步操作或者开销较大的操作时,计算属性是办不到的,要使用watch侦听器才行。

(也许是因为计算属性,只关注依赖发生变化,然后执行函数更新结果,而捕捉不到数据发生变化的中间过程?再埋一坑)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值