在vue实例上添加 _data
,指向data
,然后通过_ data
得到一个深度劫持的对象,然后再把组件实例需要的方法和 _ data
添加到这个深度劫持的对象中,这样就得到了一个组件实例,当我们读写组件实例里的这些属性,就会触发object.defineproperty()
的getter
和setter
,通过getter
和setter
来读写 _data
的属性,下面是简单的代码实现。
function deepdefineobj(_data,isaddconfig=false,config={}){
let vc // 组件实例
isaddconfig?vc={...config,_data} : vc=Array.isArray(_data)?[]:{}
// 深度劫持_data
for(let k of Object.keys(_data)){
Object.defineProperty(vc,k,{
get(){console.log('read',_data[k])
return _data[k]},
set(v){console.log('write',v)
_data[k]=v}
})
``const isobj=Array.isArray(_data[k]) || Object.prototype.toString.call(_data[k])==='[object Object]'
if(isobj) _data[k]=deepdefineobj(_data[k])
}
return vc
}
const _data=data={ // data选项的数据源
name:"xm",
age:18,
son:['a','b'],
wife:{name:'hh',age:18,sister:['e','f']}
}
const vc=deepdefineobj(_data,true,{$emit:'emit event'}) // 创建组件实例
console.log(vc)
console.log(data)
console.log('----------')
vc.age=20 // 触发响应式
console.log('----------')
vc.son[0] // 触发响应式
vc.wife.sister[0] // 触发响应式
console.log('----------')
vc.son.push('c')
//这里并没有触发son属性的写,虽然是改变原数组的方法,但数组地址没变
在vue2中改变原数组的方法,如:push、unshift
具备响应式,无疑是vue做了特殊处理。在vue中,经过打印和使用我们可以发现,对于数组的劫持使用的并不是 defineProperty,push、unshift
等方法具备响应式是因为vue对数组原型上的这些方法进行了重写
初始化时,vue根据data对象在组件实例上做了深度劫持,根据 Object.defineProperty()
,当我们做读写操作时,vue可以捕获到,但如果是进行增删操作,vue无法捕获,这时数据就不具备响应式。
下面看一个例子:
<p>{{user}}p>
<p>{{arr[0]}}p>
<button @click="changeitem">通过索引值改变数组项以及在内存里向data添加数据,看看是否具备响应式button>
<script>
data:{
user:{name:"xm",age:18},
arr:[1,2,3,4,5,6]
},
methods:{
changeitem(){
this.arr[0]=8
this.user.id="1"
}
}
script>
点击按钮后,我们会发现页面两处都没有更新
解决方案:
对于数组:vue对数组的某些方法进行了处理,使得改变原数组的方法具备响应式,如:push、unshift
其他情况,可以使用this.$set
和this.$delete
方法,this.$set(target,key,value)
第一个参数是需要劫持增删的目标对象,第二个参数是要添加的属性名,第三个参数是属性名对应的属性值,使用后,target[key]
也是具备响应式的
在vue3里解决了vue2响应式数据限制的 所有 问题,无论是对象还是数组不再需要使用$set
和$delete
,也不再需要依赖于重写的数组方法来达到数组的响应式了。下面是简单代码实现
function deepproxy(_data){
for(let [k,v] of Object.entries(_data)){
const isobj=Array.isArray(v) || Object.prototype.toString.call(v)==='[object Object]'
if(isobj) _data[k]=deepproxy(v)
}
const data=new Proxy(_data,{
get (_data,k,proxy){console.log('read',_data[k]);return Reflect.get(_data,k)},
set (_data,k,v,proxy){console.log('write||add',v);Reflect.set(_data,k,v)},
deleteProperty(_data,k,proxy){console.log('delete',k);return Reflect.deleteProperty(_data,k)}
})
return data
}
const _data={ // 组件数据源
name:"xm",
age:18,
son:['a','b'],
wife:{name:'hh',age:18,sister:['e','f']}
}
const data=deepproxy(_data) // 创建响应式数据
console.log(data)
console.log('---------')
data.son // 触发响应式
data.wife.sister // 触发响应式
console.log('---------')
data.wife.sister[2]='h' // 触发响应式
console.log('---------')
data.toString() // 触发响应式
//read function toString() ,这里可以看到,proxy可以劫持到原型上属性的增删改查
//console.log(reactive(_data)) // Proxy {name: 'xm', age: 18}
//使用 Reflect 操作对象的属性,是因为该操作如果出错不会阻塞程序运行,而直接操作对象属性的方式出错会阻塞程序
这是我对vue响应式数据原理的简单总结。最后感谢大家的观看,希望这篇文章能给你带来帮助。如果有小伙伴有一些疑问或建议,欢迎提出和分享。