Vue: 深入理解数据响应式

通过这篇文章,深入理解一下options.data,方便之后学习options的进阶属性。

主要原理在文档的深入响应式原理章节可以找到,但是本篇文章扩充了非常多的内容。

Vue 对 data 做了什么

重要议题

小实验 - data 变了

  • 示例代码: myData居然变了
  • 一开始是{n:0},传给new Vue之后立马变成{n: (...)}

{n: (...)}是个什么玩意,为什么表现和{n:0}一致?

我们需要先看一下ES6getter 和 setter

  • 示例代码
  • 学完之后呢?还是不理解{n: (...)}呀。

我们需要再看一下Object.defineProperty() | MDN

现在理解了吗?

示意图

如果data有多个属性nmk,那么就会有get n,get m,get k

getter 和 setter

定义的时候可以直接去设置一个gettersetter

来两个例子:

const obj = {
  log: ["a", "b", "c"],
  get latest() {
    if (this.log.length === 0) {
      return undefined;
    }
    return this.log[this.log.length - 1];
  },
};

console.log(obj.latest);
// expected output: "c"
const language = {
  set current(name) {
    this.log.push(name);
  },
  log: [],
};

language.current = "EN";
language.current = "CN";

console.log(language.log);
// expected output: Array ["EN", "CN"]

Object.defineProperty

定义完了之后,你又突然想加一个gettersetter,就可以使用 Object.defineProperty

Object.defineProperty() | MDN

小结

Object.defineProperty

  • 可以给对象添加属性value
  • 可以给对象添加getter / setter
  • getter / setter用于对属性的读写进行监控

代理和监听

  • myData对象的属性读写,全权由另一个对象vm负责
  • 那么vm就是myData的代理
  • 比如不用myData.n,偏要用vm.n来操作myData.n

vm = new Vue({data: mydata})

  • 首先,会让vm成为myData的代理(proxy)
  • 其次,会对myData的所有属性进行监控

Q: 为什么要监控?

A: 好问题,为了防止myData的属性变了,而vm不知道。

Q: vm知道了又如何?

A: 你是不是傻,知道属性变了,就可以调用render(data)了呀。

  • UI = render(data)

数据响应式

什么是响应式

  • 如果一个物体能对外界的刺激做出反应,它就是响应式的

我打你一拳,你会喊疼,那你就是响应式的:)

Vue 的 data 是响应式

  • const vm = new Vue({data:{n:0}})
  • 如果修改vm.n,那么UI中的n就会响应
  • Vue 2通过Object.defineProperty来实现数据响应式

什么是响应式网页

  • 如果用户改变窗口大小,网页内容会做出响应,那这个网页就是响应式网页。

比如著名的响应式网页: Smashing Magazine

Vue 的 data 的 bug

Object.defineProperty 的问题

Q: 对于 Object.defineProperty(obj, 'n', {...}),必须要有一个n,才能监听和代理obj.n对吧,可是如果前端开发者差点意思,没有给n怎么办?比如下面这两个例子:

示例一: Vue会给出一个警告

示例二: Vue只会检查第一层属性

Q: 那此时如果我点击set b,请问视图中会显示1吗?

A: 不会。因为Vue没有办法监听一开始就不存在的obj.b

那怎么办

  1. 我把 key 都声明好,后面不再加属性不就行了

  2. 使用Vue.set或者this.$set

Vue.set 和 this.$set

  • 作用一: 新增key
  • 作用二: 触发UI更新(但不会立刻更新)
  • 举个例子:
console.log(Vue.set === this.$set);
Vue.set(this.obj, "b", 1);
this.$set(this.obj, "b", 1);

完整代码 - 自己试试

data 中有数组怎么办

Q: 刚才说了如果data中新增了key有两种方法可以解决,但有没有可能没有办法提前声明好,只能使用set呢?比如说data里有数组怎么办?

A: 好问题,继续看。

  • 你没法提前声明所有key:

示例 1: 数组的长度可以一直增加,下标就是key,你看,你没有办法提前把数组的key都声明出来,Vue也不能检测对你新增了下标,难道每次改数组都要用Vue.set或者this.$set?

  • 尤雨溪的做法:

篡改数组的API,见文档中变更方法,这 7 个API都会被Vue篡改,调用后会更新UI.

  • 篡改的ES6写法:

不代表Vue的真实实现,只是说个思路

class VueArray extends Array {
  push(...args) {
    const oldLength = this.length; // this就是当前数组
    super.push(...args); //会调用上一层原型(原来数组)的push方法
    console.log(" push ");
    for (let i = oldLength; i < this.length; i++) {
      Vue.set(this, i, this[i]); // 将每个新增的key都告诉Vue
    }
  }
}

可以看到,Vue中的push()做了两件事,第一个是调用以前的push(),同时还帮你把新增的key通过Vue.set或者this.$set通知给Vue

  • 结果:

你还是可以通过this.array.push()就可以完成对数组增的操作,但其实你要明白,这个push() 其实已经不是最原始的那个push() 了。

你: push() 你变了

push(): 没错,我变了。但是,你还是可以像以前对我那样对我[旺柴]

总结

对象中新增的 key

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

数组中新增的 key

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

注意,this.$set作用于数组时,并不会自动添加监听和代理,原因未知,可以问尤雨溪:)

使用 Vue 提供的变更方法 API 时,会自动添加监听和代理

说说对 Vue 数据响应式的理解

好了,写了这么多,最后说说我对Vue数据响应式的理解

  1. Vue通过使用getter&setterObject.defineProperty来追踪data的变化,这样来实现响应式
  2. Vue通过使用Vue.set或者this.$set来新增key,并且创建监听和代理,触发UI更新(不会立即更新)

扩展阅读: Vue 数据响应式中的一个刁钻的题 - 知乎

comments powered by Disqus