🌞

理解 Vue 的 data 与 响应式

聊聊 options 中的 data。


Key Points

  1. Vue 的 data 是响应式的,如果改变 data,那么 UI 就会刷新
  2. Vue 2 通过 Object.defineProperty 实现数据响应式
  3. Vue 2 会将原来的属性变为 getter-setter 属性,并创建一个代理来操纵数据

可参考: Vue 文档 - dataVue 文档 - 深入解响应式原理Vue 文档 - 列表渲染

getter 和 setter

由于 getter 和 setter 是 Vue 数据响应式中运用到的非常重要的一环,我们得先把这两个东西搞清楚。

通过下面这段代码,我们可以很清楚地了解到 get 和 set 其实就是两个函数:get 返回了一个拼凑的字符串,set 则修改了 person 内部的 firstName 和 lastName 两个真实存在的属性——但是这两个函数的定义和调用稍稍有点特别。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    const person = {
      firstName: 'Harvey',
      lastName: 'Zhang',
      get name() { // getter
        return this.firstName + this.lastName
      },
      set name(newName) { // setter
        newName = newName.split(' ')
        this.firstName = newName[0] 
        this.lastName = newName[1]
      }, 
      age: 18
    }
    
    console.log('name: ' + person.name) // 这就是 getter,我们在调用的时候不需要再使用 obj.name()
    person.name = 'Kate Wu' // 这就是 setter,用 = 'xxx' 触发 set 函数

我们可以看到 person 中的 name 表示得有点特别,事实上他并不是一个真实存在的属性。

1
2
3
4
5
6
7
8
9
    {
      name: (...), // name 不是一个真实的属性,但我们确实可以读写 name
      firstName: "Kate",
      lastName: "Wu",
      age: 18,
      get name: ƒ name(),
      set name: ƒ name(newName),
      __proto__: Object
    }

Object.defineProperty

我们可以使用 Object.defineProperty() 方法来给对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象,详细的用法可以参照 MDN 文档,我们这里只介绍影响后续理解的部分。

基本语法是:

1
2
3
  Object.defineProperty(obj, prop, descriptor)
// Object.defineProperty
// (要在上面定义属性的对象, 要定义或修改的属性的名称, 将被定义或修改的属性描述符)

这里需要介绍一个概念—— 描述符。目前我们的描述符有两种:数据描述符存取描述符

  • 数据描述符 是一个具有值的属性,该值可能是可写的,也可能不是可写的——也就是类似于我们常见的那种普通属性。

    1
    2
    3
    4
    5
    6
    
    let data = {}
        
    Object.defineProperty(data, 'n', {
      value:0
      // 数据描述符,这里的意思是:给 data 定义一个新属性 n,他的值为 0
    })
    
  • 存取描述符 是由 getter-setter 函数对描述的属性——也就是上文说的并不真实存在的属性。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    let data = {}
        
    data._n = 0 // _n 用来存实际上不存在的 n 的值
        
    Object.defineProperty(data, 'n', {
      get(){ // getter-setter 函数是存取描述符,可以用来在存取的时候做校验
        return this._n
      },
      set(value){
        if (value < 0) return
        this._n = value
      }
    })
    

至此,我们学会了使用 getter-setter 来为函数新增属性,但是我们可能还需要借助代理,并且监听原来的属性来防止直接对 this._n 进行篡改。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    let myData = {n: 0}
    let data = proxy({ data: myData }) // data 即为 myData 代理
    
    function proxy({data}) { // 解构赋值,data 即为 myData,即为 {n: 0}
      let value = data.n // 存下 n 原来的值
      delete data.n // 删除掉 data 中原来的 n,这句可以不用写,因为下面声明新的虚拟 n 的时候会删掉之前的
      // 声明一个虚拟的 n,放到 myData 上面,防止直接修改 myData(监听 myData)
      Object.defineProperty(data, 'n', {
        get(){
          return value
        },
        set(newValue){
          if(newValue < 0) return
          value = newValue
        }
      })
      // 声明代理
      const obj = {}
      Object.defineProperty(obj, 'n', {
        get(){
          return data.n
        }
        set(value){
          if (value < 0) return
          data.n = value
        }
      })
      return obj // obj 就是代理
    }

Vue 中是怎么做的

1
vm = new Vue({ data: myData })

从原理上讲其实是差不多的,Vue 拿到我们传入的 myData 之后,做了这么几件事情:

  1. vm 成为 myData 的代理,以后我们用 vm.xxx 或者 this.xxx 就可以直接操作原来在 myData 中的数据了;
  2. 删除掉 myData 上原来的所有属性,并改成 getter-setter 属性,防止 myData 上面的属性被越过 vm 直接篡改;
  3. 这么做的好处是可以让 vm 知道属性变化之后触发 render

Vue-data

问题

事实上因为 Object.defineProperty() 其实是有一些问题的,因此 Vue 中也会存在这些问题,尽管 Vue 已经对他们进行了处理,但仍然应当注意。

  • 如果最开始属性不存在,后来想要加属性,那么新加的属性就没有被 getter-setter 化,因此就不具备响应式。

    1
    2
    3
    4
    5
    6
    7
    8
    
    const vm = new Vue({
      data: {
        a: 1
      }
    })
        
    vm.b = 2
    // vm.b 不是响应式的
    

解决方法:要么最开始就把所有的属性名写好,要么就使用 Vue.set(this.obj, 'key', 'value') 或者 vm.$set(this.obj, 'key', value) 来添加新的属性。

  • 对于数组来说,使用 arr[index] = value 来添加值将同样不会被检测到。

解决办法:Vue 实际上已经给我们的数组加上了一层新的原型,并提供了 push pop shift unshift splice sort reverse 这个 7 个常用的 API。

updatedupdated2020-01-312020-01-31