Description
相信玩过vue的都知道,vue的数据和视图都是双向绑定的,也就是说当数据(data)发生更改时,vue会自动将更改diff到视图层上。那么vue是怎么自动检测到他的数据变动的呢?在这个问题上,angluar用的是脏检查(dirty check),也就是轮询检测,性能较低,而knockout用的是ko.observable
函数(兼容IE6还要什么自行车),而vue则用的是Object.defineProperty
。
其实在很久之前就听说过Object.defineProperty
这个属性,但是只知道是个es5新属性(这也就是为什么vue不兼容IE9的原因之一),具体能干什么没有深究。直到上个学期末,考完试后有两个星期的空余时间,于是打算造个mvvm轮子(也就是后来的Zeta),当时深挖vue双向绑定原理的时候也好好研究了一番这个Object.defineProperty
。
Object.defineProperty是什么
正如他的名字一样,Object.defineProperty
是为对象设置一些默认的属性,如writeable
(可写)和getter
(访问器)等,也就是说,Object.defineProperty
是用作扩展原生对象的一种方法。
使用方法
Object.defineProperty(obj, prop, descriptor);
obj
需要定义属性的对象。prop
需被定义或修改的属性名。descriptor
需被定义或修改的属性的描述符。
问题来了,描述符
是个什么东西,有什么用?我们先看看官方定义:
configurable: 仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除。默认为 false
enumerable: 仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false
value: 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined
writable: 仅当仅当该属性的writable为 true 时,该属性才能被赋值运算符改变。默认为 false
get: 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。undefined
set: 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为undefined。
官方描述已经很清楚了,我们可以为一个对象单独设置访问器和设置器,限制读写限权或者设置默认的值。这些特性都将十分有用,我们可以重写对象的`getter`和`setter`,拦截对象的读写情况,也就是等于在对象外面包了一层机关,所以也有人将`Object.defineProperty`作为**对象拦截器**。
vue也是通过改写data的`getter`和`setter`,监听data对象里面所有属性的变动。vue在`getter`里面收集所有属性依赖,然后`setter`里面发布更新信息,做到同步更新视图。根据这个思路我们可以尝试做一个简单的对象读写拦截器。
用Object.defineProperty监听对象的读写
我们先创造一个对象,用作监听:
const obj = {
name: 'phenom',
age: 20
};
这个对象里有两个属性,一个是name
,一个是age
。
接着我们用for in来遍历一下
obj`,让其每一个属性都装配上拦截器:
for(let key in obj) {
let oldVal = obj[key];
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
return oldVal;
},
set: newVal => {
if(newVal != oldVal){
console.log(`${key}从${oldVal}改变为${newVal}`);
oldVal = newVal;
}
}
});
}
可以很清楚看到这个拦截器是怎么工作的,首先设置enumerable
和configurable
都为true
(不然怎么被遍历到),然后把当前的值保存到oldVal
(这个步骤并不是必要,只是在很多时候都要用到上一次修改的值,这里是为了演示)。然后getter
很直接地返回当前的值,在setter
里面有一个判断,如果新设置的值不等于当前的值才会把新值赋应用到当前。
注:上面的let不能直接改成var,这里涉及到js的作用域和闭包问题
之后我们来走一波试试,首先我们获取`obj.name`:
console.log(obj.name);
console.log(obj.age);
之后我们来改动一下obj
的属性:
obj.name = 'Marshmallow';
obj.name = 'Nougat';
obj.age = '30';
obj.age = '40';
十分神奇哈哈,现在setter
能够捕获到属性的每一次更改情况。
总结
说了这么多,那么究竟Object.defineProperty
能用在什么场景呢?就我平时在写轮子的时候总结出来我用到Object.defineProperty
的场景有:
1. 在设计MVVM框架的时候做数据双向绑定。
2. 使对象内的所有属性变成只读(const只能使对象变成只读,不能影响对象内的属性)
3. 限制state
(状态)的修改权限,阻止直接赋值修改,限制只能用setState
方法修改(在设计类React框架的时候很有用)。