我对 Vue 数据响应式的理解

173 阅读6分钟

我认为数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制。--详情请看官方文档

如何追踪变化

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用Object.defineProperty把这些 property 全部转为 getter/setter。这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

1. 什么是 getter、setter

getter是获取某个特定属性的值的方法; setter是设定某个属性的值的方法。
用代码说明一下:

let obj1 = {
  姓: "高",
  名: "圆圆",
  age: 18
}

声明一个对象,如果我想得到该对象的姓名,可以在该对象里再添加一个名为“姓名”的函数,调用它即可:

let obj1 = {
  姓: "高",
  名: "圆圆",
  姓名(){
    return this.姓 + this.名
  },
  age: 18
}
console.log(obj1.姓名()) //打印出 高圆圆

使用 getter 也可以达到相同的效果:

let obj1 = {
  姓: "高",
  名: "圆圆",
  get 姓名(){
    return this.姓 + this.名
  },
  age: 18
}
console.log(obj1.姓名)  //打印出 高圆圆

getter 的用法就是这样,通俗来说,就是一个调用时不需要加括号的函数

同理,setter 的用法也大致相同:

let obj1 = {
  姓: "高",
  名: "圆圆",
  get 姓名() {
    return this.姓 + this.名;
  },
  set 姓名(xxx){
    this.姓 = xxx[0]
    this.名 = xxx.slice(1)
  },
  age: 18
}
obj1.姓名 = '刘诗诗'
console.log(`姓 ${obj1.姓},名 ${obj1.名}`) //打印出  姓 刘,名 诗诗

用 = xxx 触发 set 函数

注意:  “姓名” 并不是一个真实的属性,因为 obj1 在定义时并没有写“姓名”这个属性,这个意思是说我们可以通过 get 和 set 对“姓名”属性进行读和写,但是并不存在一个叫“姓名” 的属性

2. Object.defineProperty 怎么用

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

Object.defineProperty(obj, prop, descriptor)

举个例子:在对象 obj1 中增加 getter、setter

let _xxx = 0;//可声明一个局部变量或者全局变量来使用,不可直接用 xxx
Object.defineProperty(obj3, "xxx", { 
  get() {
    return _xxx;//新定义的xxx是不存在的,不能在get|set里return xxx,会死循环
  },
  set(newValue) {
    _xxx = newValue;
  }
});

3. Vue 如何追踪依赖

Vue 将 data 转为 getter/setter 后,这些 getter/setter 如何做到能够让 Vue 追踪依赖,用 代码 解释一下大概的思路:

用 Object.defineProperty 给 data 对象定义属性 n

let data = {}

Object.defineProperty(data, 'n', {
  value: 0
})

实现需求:

n 不能小于 0,即 data.n = -1 应该无效,但 data.n = 1 有效

let data = {}

data._n = 0 // _n 用来偷偷存储 n 的值

Object.defineProperty(data, 'n', {
  get(){
    return this._n
  },
  set(value){
    if(value < 0) return
    this._n = value
  }
})

image.png

但是,如果直接修改 data._n,也可以修改 data.n:

image.png

怎么禁止?

可以使用代理:

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

function proxy({ data }) {
  const obj = {}
  Object.defineProperty(obj, "n", {
    get() {
      return data.n
    },
    set(value) {
      if (value < 0) return
      data.n = value
    } 
  })
  return obj // obj 就是代理
}

data1.n    //0

data1.n = -1
data1.n    //0

data1.n = 1
data1.n    //1

注释: proxy 函数返回 obj 对象,第一行代码中 data1 赋值为 obj,所以 data1 就是 obj,打印 data1.n 就是打印 obj.n,而操作 obj.n 又相当于操作 data.n,所以第一次打印出 0;第二次打印出 0(设置为 -1 失败);第三次打印出 1(设置为 1 成功)

使用代理也不能完全保证设置 data.n = -1 无效,例如:

上面代码中,如果把匿名函数 { data: { n: 0 } } 命名:

let myData = { n: 0 };
let data1 = proxy({ data: myData })

那么直接修改 myData 即可修改 data.n,如何避免这种情况?

在代理函数中监听一下 data :

let myData = { n: 0 };
let data1 = proxy2({ data: myData });

function proxy2({ data }) {
  let value = data.n;
  delete data.n; //这句话多余了,不写的话创建虚拟属性 n 时,也会覆盖之前的属性 n
  Object.defineProperty(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;
}

data1.n   //0
myData.n = -1;   //触发 data.set n 设置 data,但是加了 if 条件,设置失败
data1.n  //0,设置为 -1 失败了 
myData.n = 1; //触发 data.set n 设置 data,设置成功
data1.n   //1,设置为 1 成功了 

注释: 在监听 data 的代码部分中,使用了一个新的变量 value 储存 data.n 的数值,并且 data.get/set n 都是对 value 进行操作,这相当于 value 对 data.n 进行代理,所以我们对 myData 进行修改也可以被监听到了

4. 总结

let data1 = proxy2({ data:myData })
let vm = new Vue({data: myData})

对比一下上面两行代码:

当我们把数据 myData 传给 Vue 后,会让 vm 成为 myData 的代理(proxy);会对 myData 的所有属性进行监控;当 myData 的属性改变之后,会触发 setter 并通知 watcher,从而使它关联的组件重新渲染。

检测变化的注意事项

我们知道 Vue 是通过 Object.defineProperty 来实现数据响应式的,由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。

1. 对于对象

由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化(使用 Object.defineProperty(obj, 'n' , {...})),必须要有一个 'n',才能监听 & 代理 obj.n ,假如我们没有写 n 呢?

两种情况:

①没有写 n ,但是用了,就会警告:(n 没有定义,但是引用了)

image.png ②在 data 中只定义了 obj.a,但用到了 obj.b:(不会报错,因为 Vue 只会检查第一层属性) 此时如果点击 set b,会显示 1 吗?

image.png 答案是:不会

因为 Vue 没法监听一开始不存在的 obj.b,对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。

解决办法:

①把 key 都声明好,后面不再加属性 image.png ②使用 Vue.set 或者 this.$set image.png

注意:

2. 对于数组

在数组中,没有办法提前声明所有的 key,例如

image.png 点击 set d 按钮后,数组中会增加 "d" 选项吗?

不会,因为 Vue 不能检测到你新增了下标

解决办法:

①用 Vue.set 或者 vm.$set image.png ②使用 push

image.png 注意:

Vue 有 7 种数组变更方法,当你把数组传给 Vue 后,Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

3. 总结

对象中新增的 key:

  • Vue 没有办法事先监听和代理
  • 要使用 set 来新增 key,创建监听和代理,更新 UI
  • 最好提前把属性都写出来,不要新增 key

数组中新增的 key:

  • 也可用 set 来新增 key,更新 UI
  • 不过尤雨溪篡改了 7 个 API 方便你对数组进行增删
  • 这 7 个 API 会自动处理监听和代理,并更新 Ul
  • 结论:数组新增 key 最好通过 7 个 API