Vue 中的数据 data 是响应式的,Vue 通过 Object.defineProperty()函数 来用 getter和setter方法 对 data 数据做了代理和监听,一旦数据发生变化,Vue 就会改变数据对应的 UI 视图,这就是 Vue的数据响应式

Vue如何实现数据响应式

getter和setter

getter和setter 方法是以 get 和 set 关键字来为对象添加虚拟属性的一种方式。
这种属性其实并不真实存在,而是以取值函数 getter 和存值函数 setter 来模拟的一种属性。
目的是对某个属性设置存值函数和取值函数,拦截该属性的存取行为,以便于对该属性的存取做一些限定处理。

getter方法

get关键字隐式调用getter函数以读取对象属性
触发方式:点语法,即读属性即可,不用加括号

  1. const obj = {
  2. log: ['a', 'b', 'c'],
  3. get latest() { //在正常方法前加 get 关键字
  4. if (this.log.length == 0) {
  5. return undefined;
  6. }
  7. return this.log[this.log.length - 1];
  8. }
  9. }
  10. console.log(obj.latest);// 输出 c,获取的是属性名不用带括号

setter方法

set关键字隐式调用setter函数以写对象属性
触发方式:首先在对象内声明setter方法后,方可根据set后赋值的对象属性变更作为依据(对属性做修改)触发setter函数

  1. const language = {
  2. set current(name) {
  3. this.log.push(name);
  4. },
  5. log: []
  6. }
  7. language.current = 'EN';
  8. language.current = 'FA';
  9. console.log(language.log);//输出 Array ["EN", "FA"]

Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

Object.defineProperty(obj,‘a’,{value:2,writable:true,configurable:true})
第三个参数就是属性描述符对象 ,首先它是个对象,
它有自己的属性,其次它是属性a的属性描述符,用来配置属性
通俗解释
‘a’是虚拟属性;第三个参数用来配置这个虚拟属性,一般通过get和set操作配置这个虚拟属性
注意:
在定义get和set的时候不能直接使用a虚拟属性,
因为使用了就会回去调用里面get和set,然后进入死循环,
解决办法:随便找一个局部变量或全局变量来偷偷盛放虚拟属性的值即可

添加常规属性:

  1. let data = {
  2. m: 0
  3. }
  4. Object.defineProperty(data, 'n', {
  5. value: 1 //添加属性的 value 就是其值
  6. })
  7. console.log(`${data.n}`) //则会输出n值为1

也可用来添加 getter 和 setter 的虚拟属性(常用):

  1. let data1 = {
  2. _n: 0
  3. }
  4. Object.defineProperty(data1, 'n', {
  5. get(){
  6. return this._n
  7. },
  8. set(value){
  9. if(value < 0) return
  10. this._n = value
  11. }//直接写明 get / set 即可
  12. }) //由于指明了虚拟属性为 n,即 get n(){}、set n(value){},因此在函数定义时就不用再写n了

Vue对数据的代理和监听

代理

代理,即Proxy,

  • 简单来说我自己的一些事情我自己不亲自处理,而是交给一个人让他去帮我做,那个做事的人就是代理。
  • 这个逻辑中有两个关键点需要搞清楚,代理方和委托代理方,代理是处理操作的人,而其处理操作的事情不属于他,而是属于委托代理方的。

类比到 Vue数据代理 ,

  • 委托代理的是 data{} 数据对象,其代理就是 Vue实例vm ,data{} 数据对象要代理 vm 做的事情是管理 data{} 数据对象里数据操作。
  • 因此 data{} 数据对象只负责内部数据的生产即可,对生产出来的数据的管理和操作全权交给 vm 处理。

监听

那么 vm 如何对 data{} 数据对象里的数据进行控制和操作呢?换句话说,vm 如何在 data{} 数据对象里面的任意一个属性值变化时都及时知道呢?

  • 于是便用到了 ES 语法中的 getter和setter 方法,通过 getter和setter 方法控制的属性的任何操作都会被这两个函数检测到。

  • 而 getter和setter 方法形成的属性是虚拟属性,真实并不存在,因此如果用户想私自不经过代理 vm 直接修改 data{} 数据对象的属性也获取不到对应的实体属性,只能通过 getter和setter 方法修改,那么其修改就必定被 vm 检测到。

  • 因此 vm 为了实现对 data{} 数据对象里数据的全部控制,就必须在 Vue实例 创建的时候对传进来的 data{} 数据对象做一些处理,做的处理就是将 data{} 数据对象里的属性都变成了 getter和setter 方法控制的虚拟属性,并保存在代理数据对象 obj 并返回。

  • 但为了不让用户直接修改原来的 data{} 属性,也将原来的 data{} 对象的实体属性全改变了,添加的虚拟属性名字和实体属性名一样,就会用虚拟属性覆盖原来的实际属性,用户在修改属性值是就是通过 getter和setter 方法修改的虚拟属性。这样一来 data{} 数据对象的全部属性的任何变化都会被 Vue实例vm 检测到。 ```javascript let myData = {n:0} let data = proxy({ data:myData }) // 类似于 let vm = new Vue({data: myData})

function proxy({data}/ 解构赋值/){ let _n = data.n Object.defineProperty(data, ‘n’, { //覆盖原来的data.n属性 get(){ return _n }, set(newValue){ if(newValue<0)return _n = newValue } })// 改变data{}数据对象本身属性,可通过闭包形成上下文,让原来的实际属性值存在闭包的上下文_n中

const obj = {} Object.defineProperty(obj, ‘n’, { get(){ return data.n }, set(value){ data.n = value } }) //添加data{}数据对象的代理,对data{}数据对象操作

return obj // obj 就是data{}的代理 }

  1. 但是 Vue 使用 Object.defineProperty 来设置监听,就只能对在 Vue实例化 data 对象里已经存在的属性设置监听,而**对不存在的或者后来添加进去的属性没有进行监听**。<br />为了**解决**这个问题,有两种方法:
  2. 1. 将所有属性都提前声明好<br />**2. 使用 Vue.set this.$set 添加属性**
  3. 使用` Vue.set``` 添加属性是会通知 Vue 对此后添加的属性也设置监听操作。
  4. ```javascript
  5. Vue.set('this.data','m','10')
  6. this.$set('this.data','m','10')//为vm的data对象添加属性m值为10

注意:
this.$set 作用于数组时,并不会自动添加监听和代理

数组变异

  • 对于数组的数据增加,无法控制其新增个数因此不能提前声明所有数据值,而一个一个 set 又太麻烦,而且数组是常用的对象数据类型中的一种。
  • 因此Vue 改造了数组的 api,在原型链中(最外层/上层)加入了 7 个变异方法,这些变异方法会自动调用 set,并调用原来的数组方法

image.png