vue 双向数据绑定是通过 数据劫持
结合 发布订阅模式
的方式来实现的, 也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变;
在vue2.x中数据双向绑定的核心是使用 Object.defineProperty(对象,key,{get(),set()})
方法来给对象的属性添加get
、set
方法实现的!
常用的给对象添加/修改属性有如下几个方法
const person = {
name: 'chaochao'
}
const person = {}
person.name = 'chaochao'
const person = {}
person['name'] = 'chaochao'
除了以上添加属性的方式,还可以使用Object.defineProperty定义新属性或修改原有的属性
Object.defineProperty(obj,key,{
value: undefined,
enumerable: false,
writable:false,
configurable:false, //
// 当获取该属性时就会调用get方法 ,返回值为获取到的值
get(){
return xxx
}
// 当给该属性进行赋值时就会调用set方法
set(val){
}
})
value: 属性默认值,默认为undefiend
Object.defineProperty(person, 'name', {
value: 'chaochao'
})
console.log('name', person.name) // 'chaochao'
writable: 该属性是否可以被修改,默认值为false
Object.defineProperty(person, 'name', {
value: 'chaochao'
})
document.getElementById('btn').onclick = function(){
person.name = 'niuniu'
console.log('name', person.name) // 'chaochao'
}
上述示例中点击按钮修改name属性值却没有修改成功,原因是writable属性值为falae --> 不可以修改属性值
Object.defineProperty(person, 'name', {
value: 'chaochao',
writable: true
})
此时再修改就可以成功了
enumerable: 该属性是否可以枚举(在循环便利时是否可以获取到),默认值为false
const person = {
age: 20
}
Object.defineProperty(person, 'name', {
value: 'chaochao'
})
for(let key in person){
console.log('key', key) // 只能遍历到age属性,遍历不到name属性
}
configurable: 该属性是否可以被删除,默认值为false
get/set: 是属性的拦截器
当要读取该属性值时就会走get方法,方法的返回值就是读取的值
当要修改该属性值时就会走set方法,可以在此判断是否修改值
此时需要注意在get和set方法中不要直接获取该属性,否则会陷入死循环
let person = {}
Object.defineProperty(person, 'age', {
get(){
return person.age
},
set(val){
person.age = val
}
})
console.log('person', person.age)
此时会报错:Maximum call stack size exceeded
原因是当通过点语法获取age属性时就会走到get方法,而get方法中又通过点语法获取数据,就会一直循环下去
let person = {
_age: 18
}
Object.defineProperty(person, 'age', {
get(){
return person._age
},
set(val){
person._age = val
}
})
console.log('person', person.age)
以上代码就可以正常获取了
其实使用这个语法,可以防止用户不合理的修改,比如这个年龄,若是用户输入小于0的值就可以不修改
set(val){
if (val>0) {
person._age = val
}
}
tips:
value + writable属性不能和 get+set属性共存
let person = {
_age: 18
}
Object.defineProperty(person, 'age', {
value: person._age,
get(){
return person._age
},
set(val){
person._age = val
}
})
console.log('person', person.age)
此时会报错“无效属性”
原因是: value属性与set都是写入属性,这样可能存在数据冲突,因此两者不可兼容。
需求:现在有如下代码,希望person对象存在一个age属性,属性值为number变量的值且 number值与age属性值能够实时双向绑定
let number = 18
let person = {
name:'chaochao',
sex:'女'
}
实现:
<button id="btn1">editNumber</button>
<button id="btn2">editAge</button>
<script>
/*
现在有如下代码,希望person对象存在一个age属性,属性值为number变量的值且 number值与age属性值能够实时双向绑定
let number = 18
let person = {
name:'chaochao',
sex:'女',
age: 属性值与number变量双向绑定
}
*/
let number = 18
let person = {
name: 'chaochao',
sex:'女',
}
Object.defineProperty(person, 'age', {
get(){
return number
},
set(val){
number = val
}
})
document.getElementById('btn1').onclick = function(){
number++
console.log('number', number)
console.log('person', person)
}
document.getElementById('btn2').onclick = function(){
person.age = 25
console.log('number', number)
console.log('person', person)
}
</script>
结果:
在此示例中给data对象的name属性通过defineProperty进行 读取/赋值;
/*
现在有如下代码,希望person对象存在一个age属性,给age属性添加双向数据绑定
let person = {
name: 'chaochao',
sex:'女',
age: 18
}
*/
let person = {
name: 'chaochao',
sex:'女',
age: 18
}
Object.defineProperty(person, 'age', {
get(){
return person.age
},
set(val){
person.age = val
}
})
console.log('person', person)
原因:[1]点击…时调用get方法;[2]get方法内部 return person.age 在通过点语法获取age时又调用了get方法;造成死循环。
改正:
<button id="btn2">editAge</button>
<script>
/*
现在有如下代码,希望person对象存在一个age属性,给age属性添加双向数据绑定
let person = {
name: 'chaochao',
sex:'女',
age: 18
}
*/
let person = {
name: 'chaochao',
sex:'女',
age: 18
}
person._age = person.age
Object.defineProperty(person, 'age', {
get(){
console.log('@@@get@@@')
return person._age
},
set(val){
console.log('@@@set@@@')
person._age = val
}
})
console.log('person', person)
document.getElementById('btn2').onclick = function(){
person.age = 20
}
let data = {
name: 'chaochao',
sex:'女',
say:''
}
for(let key in data){
Object.defineProperty(data, key, {
get () {
return data[key] // 无限调用
},
set (str) {
data[key] = str // error: Maximum call stack size exceeded
}
})
}
改正:
<button id="btn2">editAge</button>
<script>
/*
现在有如下代码,希望person对象存在多个属性,给所有属性添加数据绑定
let person = {
name: 'chaochao',
sex:'女',
age: 18
}
*/
let person = {
name: 'chaochao',
sex:'女',
age: 18
}
Object.keys(person).forEach(key=>{
defineProperty(person, key, person[key])
})
function defineProperty(obj, key, value){
Object.defineProperty(obj, key, {
get(){
console.log('@@@get@@@', value)
return value
},
set(val){
console.log('@@@set@@@', val)
value = val // 引用数据 等价于person[key] = val
}
})
}
console.log('person', person)
document.getElementById('btn2').onclick = function(){
person.age = 20
console.log('person', person)
}
</script>
在通过new关键字实例化对象时,传入的配置项中添加了 data属性
const vm = new Vue({
el:'#app',
data:{
name:'chaochao',
sex:'女',
say:'hello word'
}
})
vue会将data存在在实例化对象的_data
属性中;
const data = {
name:'chaochao',
sex:'女',
say:'hello word'
}
const vm = new Vue({
el:'#app',
data
})
console.log(data==vm._data) // true
将data中的数据平铺在vue实例化对象身上
使用Object.defineProperty实现数据代理。
模拟数据代理过程
// 模拟配置项data
const data = {
name:'chaochao',
sex:'女',
say:'hello word'
}
// 模拟vue实例化对象 代码中更新的是_data中的数据,视图上更改的是vue实例化对象身上的数据
const vm ={
_data:data, // 存储数据
...data // 数据劫持-> 更新试图
}
for(let key in vm._data){
Object.defineProperty(vm._data, key , {
// 当获取属性值时返回vue实例化对象身上的对应的属性的属性值
get(){
return vm[key]
},
// 当修改属性值时-> 将vue实例化对象身上对应的属性值一起修改
set(val){
vm[key] = val
}
})
}
所以在vue2.x中有时会出现双向绑定失败(实例化对象中的数据改变了,但是视图上并没有重新渲染)的现象-可以使用 $set
解决
在vue3.x中数据双向绑定的核心是使用new Proxy(对象,{get(),set()})
方法来给对象的属性添加get
、set
方法实现的!
语法
let data = {
name:'chaochao',
sex:'女',
say:''
}
//data为源数据 p为代理数据
const p = new Proxy(data,{
// 拦截读取属性值 -> 当读取p身上的属性时就会触发该方法,该函数return的值就是读取到的值
// target:源数据; prop:key值
get (target, prop) {
console.log('@@@读取数据了')
return target[prop] // 默认返回的是源数据的值
},
// 拦截设置属性值或添加新属性 -> 当修改p身上的属性的属性值或给p添加新属性时就会触发该方法
set (target, prop, value) {
console.log('@@@修改数据')
target[prop] = value // 当修改了代理数据就去修改源数据的值
},
// 拦截删除属性 -> 当删除p身上的属性时就会触发该方法
deleteProperty (target, prop) {
console.log('@@@删除数据 ')
return delete[prop] // 当删除了代理数据的属性就删除源数据的属性并返回是否删除成功
}
})
可以通过Reflect对象去增删改查对象中的属性
语法
let data = {
name:'chaochao',
sex:'女',
say:''
}
// 新增属性
console.log(Reflect.set(data,'age', 18)) // true
// 获取属性
console.log(Reflect.get(data,'name'), Reflect.get(data,'age')) // chaochao, 18
// 修改属性
console.log(Reflect.set(data,'name','niuniu')) // true
// 删除属性
console.log(Reflect.deleteProperty(data,'sex')) // true
// 查看对象
console.log(data) // {name: 'niuniu', say: '', age: 18}
优点是容错性强!
new Proxy(data,{
get (target, prop) {
return Reflect.get(target,prop)
},
set (target, prop, value) {
Reflect.set(target,prop,value)
},
deleteProperty (target, prop) {
return Reflect.deleteProperty(target, prop)
}
})