phenomLi / Blog

Comments, Thoughts, Conclusions, Ideas, and the progress.
219 stars 17 forks source link

Vue数据绑定揭秘:Object.defineProperty #8

Open phenomLi opened 6 years ago

phenomLi commented 6 years ago

相信玩过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);

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。


官方描述已经很清楚了,我们可以为一个对象单独设置访问器和设置器,限制读写限权或者设置默认的值。这些特性都将十分有用,我们可以重写对象的gettersetter,拦截对象的读写情况,也就是等于在对象外面包了一层机关,所以也有人将Object.defineProperty作为对象拦截器

vue也是通过改写data的gettersetter,监听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;
        }
    }
    });
}

可以很清楚看到这个拦截器是怎么工作的,首先设置enumerableconfigurable都为true(不然怎么被遍历到),然后把当前的值保存到oldVal(这个步骤并不是必要,只是在很多时候都要用到上一次修改的值,这里是为了演示)。然后getter很直接地返回当前的值,在setter里面有一个判断,如果新设置的值不等于当前的值才会把新值赋应用到当前。

注:上面的let不能直接改成var,这里涉及到js的作用域和闭包问题


之后我们来走一波试试,首先我们获取obj.name

console.log(obj.name);
console.log(obj.age);

控制台输出: 可以正常获取到,说明getter是没问题的。

之后我们来改动一下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框架的时候很有用)。