前言
我们知道在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.n
跟vm.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)