# 数据响应式

# 什么是数据响应式

无论是组件还是实例里, 数据都是放在data选项里的. Vue 做到了使得数据能够被监听到改动, 并且触发重新渲染. 这就是数据响应式(reactivity system).

# 如何实现它

我们如此使用 Vue:

let state = { n: 0 };
const vm = new Vue({
  data() {
    return state;
  }
});
1
2
3
4
5
6

那么 Vue 构造函数会对data属性的对象进行修改, 使其能够响应式. 首先遍历对象的属性, 对于每个属性使用Object.defineProperty方法覆盖之前的属性, 我们只能使用getter&setter来对其进行操作. 然后创建一个代理对象, 也使用Object.defineProperty方法:

function proxy({data: {n: 0}}){
  let value = data.n //存住n的值, 接下来的n将不会是现在的n了
  Object.defineProperty(data, 'n', { // 之前data的n属性将由set n 和 get n 两个虚拟属性替代. 因此对n的修改会触发data的setter, 这样的改动是会被监听到的
    get(){
      return value
    }
    set(newVal){
      value = newVal
    }
  })
  let proxy = {}
  Object.defineProperty(proxy, 'n', {
    get(){
      return data.n //调用data.n的getter
    }
    set(newVal){
      data.n = newVal
    }
  })
  return proxy
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

使用代理对象操作最后返回 proxy, 我们就不对原始对象进行操作了, 转而操作这个代理, 再由代理来去操作原始对象的数据. vm对象就是一个代理, 我们可以使用vm.n = 1来改变state.n的值.因为它将触发代理的setter, 然后更新data.n. Vue 必须要知道data里所有的改动, 因此才能没有遗漏地更新视图. 如果跳过代理, 或者不使用代理. 我们使用不合规范的方法对数据改动是不会被 Vue 察觉到, 进而无法更新视图.

# Object.defineProperty的不足

我们知道, Vue 在实例化会调用Object.defineProperty:

const vm = new Vue({
  template: `
    <div>
      {{a}}
    </div>
  `
  data(){
    return {
      a: 1
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12

我们可以用vm.a = 2改变数据, 但是vm.b = 3则不会有任何改变. 因为vm.b没有被监听, 也没有被代理. 对它的处理是无用功的. Vue 提供Vue.set()vm.$set()来帮我们为没有提前声明的属性添加监听和代理, 而在Vue.set方法里会帮我们调用Object.defineProperty方法. 但是 Vue3 将会使用 ES6 的 Proxy 来取代Object.defineProperty.相信会带来新的改变.

# 变异数组

data里的数据有数组时, 而且数组的 item 是不确定的, 比如要向后端请求具体的数组时. 会和上面的情况发生冲突: 所有数据需要一开始写在data里, 然后才能监听变化. 数组里的 item 不能一开始就确定完, 从而无法被监听. 针对这种情况, Vue 提出了变异数组(mutation array)的解决方案. 它基于 JS 的 Array 内置方法封装了 7 个新的数组操作方法. 它在内部会调用对应的原生 Array 方法, 并且调用Vue.set将新的 item 加入监听里. this.arr[1] = 12 这样的写法是无效的.