MVVM数据代理实现原理

https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2FDMQ%2Fmvvm

1.数据代理

(1)前言:数据代理就是通过vm对象来代理data对象中所有属性的操作。例如我们要访问vue中data的数据应该是vm.data.msg,访问vue中的方法应该是vm.methods.getMsg(),但是实际上我们直接使用vm.msg或者vm.getMsg()来访问data或者methods中的数据。这就是数据代理
(2)实现原理:遍历要代理对象的所有key,通过Object.defineProperty为vm对象添加同名的key属性,设置get方法和set方法来动态获取或者修改data对象的属性值

<script>
function MVVM (option) {
    //保存传入的配置
    this.$option = option
    //保存data对象
    var data = this._data = this.$option.data
    //遍历data中的所有key
    Object.keys(data).forEach(key => {
        //为vm添加与key相同属性,且配置相应的属性描述符
        Object.defineProperty(this,key,{
            configurable: false,//可重新定义
            enumerable: true,//可枚举
            get () {
                return this._data[key]
            },
            set (newVlue) {
                this._data[key] = newVlue
            }
        })
    })
}

//实例化
var vm = new MVVM({
    data: {
        msg:'Hello World'
    }
})

//vm.msg相当于调用get方法返回this._data[key]
console.log(vm.msg) //Hello World
//给vm.msg赋值相当于调用set方法,更新this._data[key]的值
vm.msg = "msg is change"
console.log(vm.msg) //msg is change
</script>

2.数据劫持

数据劫持通俗的说就是监听属性的变化,然后触发相应的回调函数。例如vue模版中的数据发生了变化,页面也随之响应,他的实现原理就是对数据进行监听,一旦发生变化,则在回调函数中更新相应的dom。

为对象添加属性的方法有3种,我们前面已经学过2种:

//初始化时添加属性
var person = {name:"谢耳朵"}
//点语法添加属性
person.age = 25

console.log(person) //{name: "谢耳朵", age: 25}

这2种方法添加的属性,无论是获取属性值还是修改属性值,操作的对象都是自身。而使用第3种方法添加属性时,可以为属性设置其他的配置,包括get方法和set方法。
语法:Object.defineProperty(obj, key,options)

数据劫持的实现原理:
(1)数据代理是不能操作自身,因为调用get方法时要访问自身的值,而访问自身又会触发get方法,从而造成死循环。给属性赋值时也是一样的,set方法把新的值赋值给自己,会触发自身的set方法,也会造成死循环
(2)既然不能操作自身,那就需要为每个属性配置的一个独立的变量。无论是get方法还是set方法都是操作这个独立的变量,从而避免死循环。
(3)解决办法:为Object.defineProperty包裹一层函数,将属性值当作参数val传入。而这个val就是前面所说的独立变量,当访问get方法时可以return val。当触发set方法时,则修改val的值

<script>
//这个例子没有深度劫持
function proxyData(obj,key,val){
    //val作为一个中间变量,负责存储这个属性的值
    Object.defineProperty(obj, key, {
        configurable:false,
        enumerable:true,
        get(){
            //get方法返回这个val
            console.log('通过get方法获取了'+key+'属性的值')
            return val
        },
        set(newVal){
            //set方法修改这个val
            if(newVal === val){
                return
            }
            console.log(key+'属性的值发生了变化')
            console.log('可以在这个回调函数中做其他事件,例如更新页面dom等')
            val = newVal
        }
    })
}

var p = {name: 'kyo', age: 20}
//调用proxyData()进行数据劫持
Object.keys(p).forEach((key) => {
    var val = p[key]
    proxyData(p,key,val)
})

//获取属性值
var name = p.name
//修改属性值
p.age = 27
</script>

控制台打印:

通过get方法获取了name属性的值
age属性的值发生了变化
可以在这个回调函数中做其他事件,例如更新页面dom等

上面的方法只代理了对象的第一层属性,如果对象中包含多层对象,需要对每层次的属性都进行数据劫持的时候,就需要用到递归调用。
思路:对所有的属性进行数据劫持,如果他的属性值类型是object,则对这个属性值进行递归调用

<script>
//利用递归调用深度劫持
function proxyData(obj,key,val){
    //val作为一个中间变量,负责存储这个属性的值
    Object.defineProperty(obj, key, {
        configurable:false,
        enumerable:true,
        get(){
            //get方法返回这个val
            console.log('通过get方法获取了'+key+'属性的值')
            return val
        },
        set(newVal){
            //set方法修改这个val
            if(newVal === val){
                return
            }
            console.log(key+'属性的值发生了变化')
            console.log('可以在这个回调函数中做其他事件,例如更新页面dom等')
            val = newVal
        }
    })
    //判断属性值是否为object,如果是则对这个值进行递归调用
    if(val instanceof Object){
        Object.keys(val).forEach(key => {
            proxyData(val,key,val[key])
        })
    }
}

var data = {
    name: 'kyo',
    age: 20,
    skill: ['大蛇薙', '最终决战奥义“无式'],
    father: {
        name: '草薙柴舟',
        age: 50
    }
}
Object.keys(data).forEach((key) => {
    var val = data[key]
    proxyData(data,key,val)
})

//读取属性值
var fatherName = data.father.name
//修改属性值
data.father.age = 52
</script>

控制台打印:

//data.father.name
通过get方法获取了father属性的值
通过get方法获取了name属性的值

//data.father.age = 52
通过get方法获取了father属性的值
age属性的值发生了变化
可以在这个回调函数中做其他事件,例如更新页面dom等

猜你喜欢

转载自www.cnblogs.com/OrochiZ-/p/11853585.html