《Vue学习》数据响应式原理
数据响应式
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty
把这些 property 全部转为 getter/setter。Object.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
[tabs]
[tab title="共享描述符"]
- configurable
当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
configurable 特性表示对象的属性是否可以被删除,以及除 value 和 writable 特性外的其他特性是否可以被修改。
默认为false
。 enumerable
当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。
enumerable 定义了对象的属性是否可以在for...in
循环和Object.keys()
中被枚举。
默认为false
。[/tab]
[tab title="数据描述符"]
- value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
默认为undefined
。 - writable
当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。
默认为false
。
[/tab]
[tab title="存取描述符"]
- get
属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this
对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
默认为undefined
。 - set
属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this
对象。
默认为undefined
。
[/tab]
[tab title="描述符默认值汇总"]
- 拥有布尔值的键 configurable、enumerable 和 writable 的默认值都是
false
。 - 属性值和函数的键 value、get 和 set 字段的默认值为
undefined
。
[/tab]
[tab title="描述符可拥有的键值"]
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
数据描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
存取描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
如果一个描述符不具有 value、writable、get 和 set 中的任意一个键,那么它将被认为是一个数据描述符。如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常。
[/tab]
[/tabs]
实践
<div id="app">hello</div>
// 模拟 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
那假设是有多属性的对象呢?需要如何绑定。
核心思路就是递归遍历对象,将对象内的所有属性都绑定。
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];
},
});
});
}
简单了解数据劫持后,我们来看看完整流程。Vue 初始的时候,将 data 中的数据通过 Observe 数据劫持
后,将数据利用观察者模式来监控,当事件触发时,通过 发布者Dep
去调用 观察者Watcher
的方法来更新视图。
了解大致流程后,我们从基础出发,理解 Vue 中的设计模式: 发布/订阅模式
和 观察者模式
。后面会针对这两点继续深入。