(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侦听器才行。
(也许是因为计算属性,只关注依赖发生变化,然后执行函数更新结果,而捕捉不到数据发生变化的中间过程?再埋一坑)