# VUE 的数据代理原理

<div id="app">
    <!--swig0-->
</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 属性。实际上这个属性是由 firstNamelastName 这两个源头属性组合而成的。既然它是由其它属性组合而成的,那么我们就可以在配置对象的 data 中省略它的定义,直接在 vue 的模板中动态生成。

<div id='app'>
    <!-- 第一种方法 直接使用字符串拼接它 -->
    <p> + '-' + </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></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><!--swig4--></p>  // 1s 后这个 fullName 可以被渲染出来 输出:嘿嘿
watch:{
    firstName:{
        // 这个对象是一个配置对象
        // 当数据发生改变的时候会自动调用 handler 回调
        handler(newVal,oldVal){
            
            setTimeout(() => {
                // 异步修改数据
                this.fullName = '嘿嘿';
            }, 1000);
        }
    }
<p><!--swig5--></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){
            /*....*/
        }
    }
}
更新于 阅读次数