【Vue-data原理】存取器与Object.defineProperty

740 阅读6分钟

前言

我们知道在Vue的data中,我们的数据是双向绑定的,那么这个原理是什么呢?

现在我有一个需求,我需要设置一个obj={n:1}obj1={??},怎样才能设置obj1.n实现与obj.n双向绑定呢?

也就是当我修改了obj.n的值,那么就会同步修改obj1.n,反之亦然。

要解这个题,需要知道属性描述对象中的存取器

存取器

getter

我们先来了解存取器概念,那就是对象的getter和setter,当我们访问某个对象的虚拟属性时,可以使用get语法来调用一个函数。

let obj={
    get age(){
    	return 18
    }
}
obj.age // 18 
obj.age = 19
obj.age //18

通过上面的语法,我们可以设置一个obj.age属性,这个特点是当我访问这个属性时,会调用get age()这个函数,设置了这个属性后,我们不能直接对其进行修改。上面的代码可以看出,即使赋值修改了,也是无效的。

设置的age属性是一个伪属性,当我们没有设置setter时,它只能访问而不能修改。

setter

使用set语法可以修改设置的伪属性

let obj={
    get age(){
     // 当读取obj.age时,get起作用
    },
    set age(value){
    //  当执行 obj.age = value 操作时,setter 起作用
}

但是如果我们直接写set age(value){obj.age=value}时,会报错,因为我们的obj.age属性并不存在,它只是在我们访问时,会自动调用get后面的函数罢了。如何修复这个问题?

let obj={
	_age:18
    get age(){
    	return this._age
    },
    set age(value){
    	return this._age=value
    }
}
obj.age // 18
obj.age = 19 
obj.age //19 
age //19

你可能会问,为什么我们要在再设置一个_age属性,能不能这样写

let obj={
	age:18,
    get age(){return obj.age}
}
obj.age //Uncaught RangeError: Maximum call stack size exceeded

因为这样我们就既设置了数据属性obj.age,又设置了访问器属性,访问器属性会覆盖数据属性obj.age,当我们访问时,会进入无限次执行get age()的循环

所以我们重新定义了一个属性_age=18

setter跟getter有什么用

上面的例子可能无法很好地理解他们到底有什么用,但是实际上vue的computed属性就用到了它。我把上面的例子做一个修改

let obj={
	age:18,
	get next(){return this.age+=1},
    set next(value){return this.age =value-1}
}
obj.next //19
obj.next =20
obj.age //19

setter跟getter往往用于,属性的值依赖对象内部数据的场合。我们设置了一个新的next虚拟属性,也可以叫计算属性。当我设置它的时候,它会帮助修改数据属性,当我读取它的时候,它又会帮助我去根据内部的数据来返回给我数字。这不就类似于vue框架的computed吗?

Object.defineProperty

我们可以使用Object.defineProperty方法来通过属性描述对象,定义或修改一个属性,然后返回修改后的对象。

属性描述对象

属性描述对象就是对对象的某个属性进行描述定义,它的内容包括

  • value 属性值
  • writable 是否可写
  • enumerable 是否可遍历
  • configurable 是否可配置,它是控制属性描述对象的开关
  • get 取值函数
  • set 存值函数

注意,一旦定义了取值函数get(或存值函数set),就不能将writable属性设为true,或者同时定义value属性,否则会报错。

这里的get跟set实际上就是上面的存取器

Object.defineProperty用法

Object.defineProperty(object, propertyName, attributesObject)

参数说明:

  • object 对哪个对象使用
  • propertyName 设置的属性名
  • attributesObject 属性描述对象

示例

下面我们结合存取器来使用Object.defineProperty

我们现在要定义一个obj对象的属性p

let obj={}
obj._n=""
Object.defineProperty(obj,'p',{
	get(){return this._n },//注意这里的写法 不是get p(){}
	set(value){this._n =value}
})

我们需要对伪属性p进行读写操作,因为没办法在p属性还没生成的情况下写this.p,所以只好用_n这个属性来存储p的默认值。value为当我对属性进行赋值obj.p=18时,这个18就会当成value传给set函数。

我们可以使用存储器来控制赋值,例如,我需要obj.p只能赋值为大于0的数

let obj={}
obj._n=""
Object.defineProperty(obj,'p',{
	get(){return this._n },//注意这里的写法 不是get p(){}
	set(value){
    	if(value>0){this._n=value}else{return}
    }
})

扩展-代理

上面的代码我们使用一个obj._n来储存p的值,set经过_n,访问实际上就是访问的_n,所以我们如果修改._n的值,那么也会修改p的值。那我直接修改obj._n=-1,那么obj.p就等于-1了,那么怎么做才能避免这种问题呢

可以使用代理,我们写一个代理函数

let newName={n:1}
function proxy(newName){
	let value=newName.n//我们把newName保存到第三方里
     Object.defineProperty(newName,'n',{//覆盖掉原来的newName.n-->数据属性
     	get(){return value}, // 现在newName.n是访问器属性
        set(newValue){
        if(newValue>0){ value = newValue}else{return }
       }
     })
     let obj={}
      Object.defineProperty(obj,'n',{
     	get(){return newName.n},
        set(newValue){newName.n = newValue}
     })
     return obj // obj就是代理
}
let newObj=proxy(newName)

实际上原理就是从源头抓起,把newName的数据属性给保存到一个value上,然后把数据属性抹掉变成存取器属性,再在上面加一些判断逻辑。这个叫做监听

然后返回的新的obj就是代理。

通过这样的方法我们就创造了两个数据互相绑定的对象。

n.n === newName.n // true

跟Vue的关系

我们在上面已经知道了两个对象的数据互相绑定互相访问的原理,那么Vue里面的data跟我们写入的数据对象是不是也类似这样的原理呢?

let obj={n:1}
let vm=new Vue({data:obj}) // vue代码
let newObj=proxy(obj) // 代理代码
obj.n===vm.n
obj.n===newObj.n

上面的newObj.nvm.n都和obj.n的数据一样实现了数据的互相绑定.

  • 当我们设置了vue的data属性时,vue内部会对data的值(假设为对象a)做存取器处理,将它的内部属性给设置成getter跟setter,这样就可以监听其内部的属性
  • 然后通过代理绑定到自身的实例上。当对象a的属性发生变化时,实例身上的属性就发生了变化
  • 也就实现了数据的双向绑定。

最后的灵魂问题

那么现在我们如何完成刚开始的问题?

现在我有一个需求,我需要设置一个obj={n:1}obj1={??},怎样才能设置obj1.n实现与obj.n双向绑定呢?

1、 我们可以设置一个函数,先将obj.n这个数据属性给抹杀掉,换成存取器属性。这样就完成了监听

2、 然后通过代理返回函数的结果,这样就实现了代理

3、 最后实现双向绑定

let obj={n:1}
let obj1=(function (){
	let a= obj.n
    //监听
    Object.defineProperty(obj,'n',{
    	get(){return a},
        set(value){a=value}
    })
    //代理
    let newObj={}
    Object.defineProperty(newObj,'n',{
    	get(){return obj.n},
        set(value){obj.n=value}
    })
    return newObj
})(obj)