深入 Vue 2.x 数据响应式原理 ★

目录

1、Vue 对 data 做了什么

2、ES6 的 getter 和 setter

2、Object.defineProperty

3、proxy 代理数据的安全性

4、Vue 的数据响应式原理

5、实现一个 input 双向绑定


1、Vue 对 data 做了什么

Vue 的数据响应式是它的一大特点,也是它的一大优势,Vue 中的 data 数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图会进行更新,这使得状态管理非常简单直接,那 Vue 到底对 data 做了什么呢? Vue 官方解释:传送门

const myData = {    //将实例中的data抽出来
  n: 0
}
console.log(myData);    //页面会打印{n:10}

new Vue({
  data: myData,    //将myData传递给Vue实例的data属性
  template: ` <div>{{n}}</div> `
}).$mount("#app");

setTimeout(()=>{
  myData.n += 10;
  console.log(myData);  //页面会打印{n:(...)}
},3000)

上述实例中,第一次打印的 myData 数据是 { n:0 } 的形式,第二次打印的 myData 数据是 { n:(...) } 的形式,这中间经历了将 myData 传递给 Vue 实例的 data 属性的过程,这个过程肯定是 Vue 对 myData 做了什么导致它的形式变化,往下看!

2、ES6 的 getter 和 setter

了在解 Vue 数据响应式原理以及上述实例中的打印 myData 发生变化的原因之前,我们先来了解一下 ES6 的 getter 和 setter,对象属性可以是一个 函数gettersetter 方法。get 和 set 也是对象的属性,只不过它们是以函数的形式定义的。

let person = {                                     let obj = {  //对象属性可以是一个 函数、getter、setter 方法
  姓: "程",                                           property: function ([parameters]) {},
  名: "序员",                                         get property() {},
  get 姓名() {                                        set property(value) {},
    return this.姓 + this.名;                      };
  },
  set 姓名(xxx){
    this.姓 = xxx[0];
    this.名 = xxx.slice(1);
  }
};
console.log(person.姓名);  //打印:程序员    //getter 相当于不加 () 的函数
person.姓名 = '舒化奶';     //用 = xxx 触发 set 函数
console.log(person);       //打印:{age:18,姓:舒,名:化奶,姓名:(...)}    和Vue中的myData一样的效果

上述实例在打印 person 对象时,会发现 person 对象中多了一个 姓名 属性,它的形式和 Vue 中的 myData 的形式类似。我们将 person 对象的 姓名:(...) 属性展开,里面包含了 get 姓名:f 姓名() set 姓名:f 姓名(xxx) 

这表示,用户确实可以对 person 的姓名属性进行读写,但实际上 person 对象并不存在该姓名属性。由此类比 Vue 中的 myData 对象,打印后的 { n:(...) } 表示 Vue 实例上的 data 对象中的 n,并不会原始属性,而用户却可以通过 get/set 操作它。

那 Vue 为什么要把 data 变成这种 get/set 的形式呢,这和 Vue 的数据响应式原理有什么关系?往下看!

2、Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象,附上该属性的 mdn 文档,感兴趣的同学可以移步了解一下 → 传送门

let person = {
  _age: 18
};

//如果一个对象被声明完了之后需要对其新增 get/set... 属性的话,需使用Object.defindProperty
Object.defindProperty(person,'age',{
  value: 88,    //给 person 对象添加一个值为 88 的 age 属性!
  get(){ return this._age },
  set(value){   //这里可对定义的属性进行筛选操作,比如添加 if 语句,可以保证设置的值满足条件才生效
    if(value < 0) return;
    this._age = value 
  }
})

上述这种方式可以实现筛选功能,比如在 set() 方法中添加 if 判断语句,从而实现有条件的设置对象的属性值,如果设置的值是正数就进行赋值,如果是负数就不进行设置。但是要注意,getter/setter 是在对象声明的时候直接用的, 如果想在声明后继续给对象添加 getter/setter 属性,则需要通过 Object.defineProperty() 的方法。

3、proxy 代理数据的安全性

上述实例可以实现数据的筛选设置功能,但是并不能保证 person._age 属性的安全性,因为其他开发人员可以通过 persong._age = -1;的方式直接将其设置为一个负值,为了解决这种安全性问题,我们可以使用下面这种 proxy 代理的方法!

let data1 = proxy({ data:{n:0} }); //括号里是匿名对象,无法访问

function proxy({data}){  //{data}是结构赋值的写法
  const obj = {}
  Object.defineProperty(obj, 'n', { //这里的'n'写死了,理论上应该遍历data的所有key
    get(){
      return data.n;
    },
    set(value){
      if(value < 0) return;
      data.n = value
    }
  })
  return obj; // obj 就是代理,这样可以保证 data 中的数据不会被直接通过 data.n 的方式进行篡改
};    //但是这种方式并不是银弹!
console.log(data1.n);    //可以拿到 data1.n 属性,但是不能通过 data1.n=-1 进行篡改

let myData = {n:0};    //但是如果将data设置成一个引用值
let data = proxy({ data:myData }); //括号里是匿名对象,无法访问
myData.n = -1;    //依然可以对属性 n 进行直接篡改! 往下看!
let myData = {n:0}
let data2 = proxy({ data:myData }) // 括号里是匿名对象,无法直接访问,但是可以访问引用 myData

function proxy({data}){  //{data}是结构赋值的写法
  let value = data.n;    //获取data中的myData的n属性
  Object.defineProperty(data, 'n', {    //创建一个n的虚拟属性,进行监听,防止被偷改:delete data.n
    get(){
      return value;
    },
    set(newValue){
      if(newValue < 0) return
      value = newValue;
    }
  })    // 就加了上面几句,这几句话会监听 data

  const obj = {}
  Object.defineProperty(obj, 'n', {
    get(){
      return data.n;
    },
    set(value){
      if(value < 0) return    //这句话多余了
      data.n = value;
    }
  })
  
  return obj; // obj 就是代理
}

上述实例是只向外暴露代理对象,开发人员无法直接通过 data.n = -1;的方式设置 data 的属性。示例链接:传送门

4、Vue 的数据响应式原理

上述讲到的实例其实和 Vue 的数据响应式原理类似,如下代码:

let data2 = proxy({ data: myData }) 
let vm = new Vue({ data: myData })

实际上 Vue 在声明实例的时候:const vm = new Vue({data:myData});做了如下两件事情:

  • 一、会让 vm 成为 myData 的代理(proxy)。
  • 二、会对 myData 的所有属性进行监控,实际上是代理的内部虚拟属性。

监控的目的是为了防止 myData 的属性变了,但 vm 却不知道。如果实现监控的话,只要 myData 的属性改变都会被 vm 监听到,然后就可以调用 render(data),将数据在视图层进行重新渲染,实现数据的响应式。

  • Vue 的 options.data 在传入 Vue 后,会被 vue 监听,data 会被篡改,本来的 n 变成 get(n) set(n)
  • Vue 的 options.data 在传入 Vue 后,会被 vue 实例代理,const vm = new Vue(),vm 就是 options.data 的代理
  • options.data 的所有读写都会被 Vue 监控,不管是对 data 本身,还是它的代理都会被监控,vue会在data变化时更新UI

5、实现一个 input 双向绑定

原理都说的差不多了,然后说一个在面试过程中关于 Vue 双向绑定的一个高频题吧,毕竟纸上得来终觉浅,万一面试官让自己手写一个的话,你只会说理论很显然是不行的。

首先我们来看一下如何做到保证 视图层 => 数据层 方向的绑定,当调用属性的地方改变了这个属性(通常是一个表单元素),那么这个对象(或变量)的属性也随之改变,具体代码如下:

<input type="text" id="model">
<script>
  let user = {age: 1};
  model.value = user.age; //设置初始化默认值

  model.addEventListener('input', (event) => {    //监听 input 事件
    user.age = event.target.value;
    console.log('在 input 框内修改视图层的值!')
    console.log('此时 model 层的 user.age 为:' + user.age);
  })
</script>

然后我们再来看一下如何做到保证 数据层 => 视图层 方向的绑定,当一个对象(或变量)的属性改变,那么调用这个属性地方也应该改变,具体代码如下:

<input type="text" id="model">
<button onclick="addAge()">年龄 +1</button>

<script>
  let user = {age: 1};
  model.value = user.age; //设置初始化默认值

  Object.defineProperty(user, 'age', {
    get: function() {
      return model.value;
    },
    set: function(v) {
      model.value = v;
      console.log('此时 view 层的 model.value 为:' + model.value);
    }
  })

  function addAge() {
    user.age += 1;
    console.log('点击按钮,修改 model 层的 user.age!');
  }
</script>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值