Vue思考题_03双向数据绑定原理

[1]什么叫双向数据绑定?

  • 视图中的数据发生了变化,data中的数据也要对应改变;
  • data中的数据发生了变化,视图上的数据也要对应改变;

[2]双向绑定原理

vue 双向数据绑定是通过 数据劫持 结合 发布订阅模式的方式来实现的, 也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变;

vue2.x实现双向绑定

在vue2.x中数据双向绑定的核心是使用 Object.defineProperty(对象,key,{get(),set()})方法来给对象的属性添加getset方法实现的!

Object.defineProperty对象(es6新增)

常用的给对象添加/修改属性有如下几个方法

  • [1] 声明一个对象时添加属性
    const person = {
      name: 'chaochao'
    }
    
  • [2] 通过点语法给对象添加属性
    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都是写入属性,这样可能存在数据冲突,因此两者不可兼容。

使用Object.defineProperty进行双向绑定

需求:现在有如下代码,希望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>
vue2.x实现双向绑定

在通过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实现双向绑定缺点
  • 对象:只能给对象中已经存在的属性进行双向绑定!
    • 新增属性、删除属性,视图不会更新
  • 数组:直接通过下标修改数组,视图不会更新

所以在vue2.x中有时会出现双向绑定失败(实例化对象中的数据改变了,但是视图上并没有重新渲染)的现象-可以使用 $set 解决

vue3.x实现双向绑定

在vue3.x中数据双向绑定的核心是使用new Proxy(对象,{get(),set()})方法来给对象的属性添加getset方法实现的!

Proxy构造函数(es6新增)

语法

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对象

可以通过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}

优点是容错性强!

vue3.x双向绑定
 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)
    }
})
  • 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
  • 通过Reflect(反射): 对源对象的属性进行操作。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值