VUE的数据代理原理
<div id="app">
{{msg}}
</div>
//最简单的Vue对象
const vm = new Vue({
el:'#app',
data:{
msg:'I love you,asuka'
}
})
我们都知道当我们使用模板语法去输出msg时并不是直接输出data的msg,而是经过 vm 这层代理输出的。
我们在模板中输出的所有数据、方法都是经过 vm 代理的,而不是直接使用配置对象中东西。同样地,当我们修改 msg或者方法时,我们修改的也是 data 、methods中的东西。而不是 vm 上挂载的,修改结果将直接影响到配置对象中的值。
那么这种数据代理是如何做到的呢,答案就是 Object.defineProperty()
方法。
Object.defineProperty()
这个方法可以添加或修改一个属性,将其变为响应式属性。什么叫响应式属性呢,就是随着你源属性值的变化。它的属性值也跟随着变化。举个简单的例子
let obj = {
firstName:'zhang',
lastName:'san',
fullName:'zhang-san'
};
console.log(obj.fullName) //zhang-san
obj.firstName = 'li'
console.log(obj.fullName) //zhang-san
//我们改变firstName,合理的是输出是 li-san。但是输出的依然是zhang-san,也就是说fullName没有自动跟随firstName变化
--------------
//如果我们将fullName变成响应式属性,那么上述需求就额能实现
Object.defineProperty(obj,'fullName',{
get(){ //每次输出fullName属性时,get函数就会被调用
return this.firstName + '-' + this.lastName;
},
set(newVal){ //每次修改fullName属性时,set函数就会传入修改后的新值,然后被调用
let arr = newVal.split('-');
this.firstName = arr[0];
this.lastName = arr[1];
}
});
console.log(obj.fullName); //zhang-san
obj.firstName = 'li';
console.log(obj.fullName); //li-san
obj.fullName = 'xiao-xiangxiang';
console.log(obj.firstName); //xiao
console.log(obj.lastName); //xiangxiang
实际上我们可以 set 和 get 函数上做任何我们想做的事情。根据这个特性我们可以想到 vm 是如何做数据代理的
//模拟数据代理
let vm = {};
let data = {msg:'asuka'};
Object.defineProperty(vm,'msg',{
get(){
return data.msg;
},
set(newVal){
data.msg = newVal;
}
})
我们可以看到 vm 原本就是个空对象,但是我们使用了Object.defineProperty()
方法后,它就增添了一个属性msg
,每当这个msg
被输出时就会自动调用get
函数,被修改时就会自动调用set
函数
当我们只设置 get 函数进行操作时在 vue 中实际上就是单向数据绑定基础,而我们同时设置好 get 和 set 函数时在 vue 中就变成了双向数据绑定的基础
computed和watch
computed
我们把上例中obj
变量拿过来,在这个变量中我们定义了fullName
属性。实际上这个属性是由firstName
和lastName
这两个源头属性组合而成的。既然它是由其它属性组合而成的,那么我们就可以在配置对象的 data 中省略它的定义,直接在 vue 的模板中动态生成。
<div id='app'>
<!-- 第一种方法 直接使用字符串拼接它 -->
<p>{{firstName}} + '-' + {{lastName}}</p>
<!-- 第二种方法 用函数调用拼接,本质上和拼接字符串是一样的 -->
<p>getFullName()</p>
</div>
<script>
const vm = new Vue({
el:'#app',
data(){
return {
firstName:'zhang',
lastName:'san'
}
},
methods:{
getFullName(){
return this.firsName + '-' + this.lastName
}
}
})
</script>
当我们使用字符串拼接它有一个很大的缺点就是,一旦数据多了起来。拼接字符串这种方法维护起来十分麻烦。而在 vue 它提供了一种计算属性的方式让我们可以更加高效地得到这个由其它属性组合而来的属性。
<div id='app'>
<!-- 第三种方法 计算属性 -->
<p>{{fullName}}</p>
</div>
<script>
const vm = new Vue({
el:'#app',
data(){
return {
firstName:'zhang',
lastName:'san'
}
},
//计算出fullName,这个fullName可以不预先定义,会被动态创建
computed:{
fullName:{
get(){ //类似Object.defineProperty()
return this.firstName + '-' + this.lastName;
},
set(newVal){
/* code */
}
}
}
//当computed只使用 get 函数时,可以使用简写形式
fullName(){
return this.firstName + '-' + this.lastName
}
})
</script>
表面上看起来使用computed去计算一个属性和第一种、第二种方法比起来貌似并没有什么区别,但是实际上用计算属性的方式去获取一个全新的响应式属性效率更高,因为它会被缓存起来。当fullName
被多次使用时,它将会直接从缓存中拿出来使用不会多次使用this.firstName + '-' + this.lastName
去拼接字符串。
由上我们可以总结出computed的特性:
- 可以动态创建出一个属性,不用预先定义属性
- 创建出来的这个属性会被缓存,当属性的值不改变时使用该属性将会直接从缓存中拿出,提高效率
watch
除了上面三种方法来获得fullName
属性我还有第四种方法得到fullName
。那就是使用watch
监视属性。**使用监视的前提是这个属性必须预先存在,也就是说我们必须在data里预先就有需要监视的属性,然后才能去对它使用watch
监视。**这是和computed
的一个重要区别。继续使用上面的例子,我们只需要在data后面在跟一个watch属性
data(){
return {
firstName:'zhang',
lastName:'san',
fullName:'' //使用watch时fullName必须预先存在
}
},
watch:{//对象内放置被监视的属性
firstName:{
handler(newVal,oldVal){ //这里我们使用handler函数回调,handler是固定写法不可更改,hander会传入被监视属性的新值和旧值两个参数
//只有当firstName这个属性发生变化时,handler回调才会执行
this.fullName = newVal + '-' + this.lastName;
},
immediat:true //配置了immediate 无论被监视属性是否变化都强制执行一次handler回调
}
}
//第二种watch写法 在Vue()的外部使用$watch方法监视lastName属性
vm.$watch('lastName',function(newVal,oldVal){
fullName = this.firstName + '-' + newVal;
})
通过上面例子我们可以体会到watch的特点:
- 被监视的属性必须预先存在
- 当被监视的属性不发生变化时,handler函数其实不会被调用
- 可以使用 immediat:true 强制执行 handler 回调
computed 和 watch 最重要的区别
实际上 computed 和 watch 有一个最重要的区别就是,computed里面只能获取到同步的数据,而不能获取到异步的数据。而 watch 里面同步异步的数据都可以获取到。
<p>{{fullName}}</p> // 1s后这个fullName可以被渲染出来 输出:嘿嘿
watch:{
firstName:{
//这个对象是一个配置对象
//当数据发生改变的时候会自动调用handler回调
handler(newVal,oldVal){
setTimeout(() => {
// 异步修改数据
this.fullName = '嘿嘿';
}, 1000);
}
}
<p>{{fullName}}</p> // 1s后这个fullName不能被渲染出来 fullName的值为null,无法得到字符串 哈哈
computed:{
//计算属性的完整写法
fullName:{
get(){
let n = null;
//异步修改数据
setTimeout(()=>{
n = '哈哈';
})
return n;
},
// 当计算属性的数据能被修改时候使用(表单类元素在双向绑定计算属性值)
set(val){
/* code */
}
}
深度监视
当我们使用watch的时候如果不指定为深度监视那么它就为一般监视。
一般监视可以用const
理解,它只监视本身这个变量的引用,并不关心引用内部如何变化。如果我们使用一般监视去监视一个对象,那么它就会像浅拷贝一样,只关键浅层键值的变化,如果浅层键值又是一个对象,那么这个对象里面如何变化它是不会监视到的。
当我们使用深度监视的时候,就类似于深拷贝。它里面再套一层对象,它也能监测到。
data(){
return {
comment:[
{id:1,name:'asuhe',content:'666'},
{id:2,name:'asuka',content:'2333'}
]
}
},
watch:{
comment:{
//不开启深度监视时,只有comment数组里面的对象整个改变才能被监视到
deep:true //开启深度监视,comment里面的对象内的数据(id、)改变也能监视到
handler(newVal,oldVal){
/*....*/
}
}
}