Vue学习-数据响应式原理

Caleb ... 2020-10-22
  • Vue
大约 5 分钟

# 数据响应式

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setterObject.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式化并不同,所以建议安装 vue-devtools 来获取对检查数据更加友好的用户界面。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

# 语法

提示

Object.defineProperty(obj, prop, descriptor)

  • obj: 要定义属性的对象。
  • prop: 要定义或修改的属性的名称或 Symbol 。
  • descriptor: 要定义或修改的属性描述符。

# 先看看基础函数

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

const object1 = {};

Object.defineProperty(object1, "property1", {
  value: 42,
  writable: false,
});

object1.property1 = 77;
// 默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改(immutable)的。

console.log(object1.property1);
// expected output: 42
1
2
3
4
5
6
7
8
9
10
11
12

共享描述符

  • configurable
    当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
    configurable 特性表示对象的属性是否可以被删除,以及除 value 和 writable 特性外的其他特性是否可以被修改。
    默认为 false

  • enumerable
    当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。
    enumerable 定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。
    默认为 false


数据描述符

  • value
    该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为 undefined

  • writable
    当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。 默认为 false


存取描述符

  • get
    属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
    默认为 undefined

  • set
    属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
    默认为 undefined


描述符默认值汇总

  • 拥有布尔值的键 configurable、enumerable 和 writable 的默认值都是 false
  • 属性值和函数的键 value、get 和 set 字段的默认值为 undefined

描述符可拥有的键值

configurable enumerable value writable get set
数据描述符 可以 可以 可以 可以 不可以 不可以
存取描述符 可以 可以 不可以 不可以 可以 可以

如果一个描述符不具有 value、writable、get 和 set 中的任意一个键,那么它将被认为是一个数据描述符。如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常。

# 实践

<div id="app">
  hello
</div>
1
2
3
// 模拟 Vue 中的 data 选项
let data = {
  msg: "hello",
};

// 模拟 Vue 的实例
let vm = {};

// 数据劫持:当访问或者设置 vm 中的成员的时候,做一些干预操作
Object.defineProperty(vm, "msg", {
  enumerable: true,
  configurable: true,
  get() {
    console.log("get: ", data.msg);
    return data.msg;
  },
  set(newValue) {
    console.log("set: ", newValue);
    if (newValue === data.msg) {
      return;
    }
    data.msg = newValue;
    // 数据更改,更新 DOM 的值
    document.querySelector("#app").textContent = data.msg;
  },
});

vm.msg = "Hello World";
console.log(vm.msg);

// set:  Hello World
// get:  Hello World
// Hello World
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
30
31
32
33

那假设是有多属性的对象呢?需要如何绑定。

核心思路就是递归遍历对象,将对象内的所有属性都绑定。

let data = {
  msg: "hello",
  age: 18,
};

let vm = {};

proxyData(data);

function proxyData(data) {
  Object.keys(data).forEach((key) => {
    // 把 data 中的属性,转换成 vm 的 setter/setter
    Object.defineProperty(vm, key, {
      enumerable: true,
      configurable: true,
      get() {
        console.log("get: ", key, data[key]);
        return data[key];
      },
      set(newValue) {
        console.log("set: ", key, newValue);
        if (newValue === data[key]) {
          return;
        }
        data[key] = newValue;
        // 数据更改,更新 DOM 的值
        document.querySelector("#app").textContent = data[key];
      },
    });
  });
}
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
30
31

简单了解数据劫持后,我们来看看完整流程。Vue 初始的时候,将 data 中的数据通过 Observe 数据劫持 后,将数据利用观察者模式来监控,当事件触发时,通过 发布者Dep 去调用 观察者Watcher 的方法来更新视图。

了解大致流程后,我们从基础出发,理解 Vue 中的设计模式: 发布/订阅模式观察者模式。后面会针对这两点继续深入。

打赏