Vue 动态数据绑定(一)-- Observer

任务说明地址

任务描述
这是“动态数据绑定”系列的第一题。
我之前经常使用 Vue,后来不满足于仅仅使用它,我想了解其内部实现原理,所以就尝试学习其源码,获益匪浅。所以,如果你跟我一样,希望挑战这高难度的事情,那就开启这一系列吧!
我们从最简单的开始。
其中,动态数据绑定就是 Vue 最为基础,最为有用的一个功能。这个系列将分成5部分,一步一步来理解和实现这一功能。
ok,我们从最简单的开始。给定任意一个对象,如何监听其属性的读取与变化?也就是说,如何知道程序访问了对象的哪个属性,又改变了哪个属性? 举个例子。

let app1 = new Observer({
    name: 'youngwind',
    age: 25
});
let app2 = new Observer({
    university: 'bupt',
    major: 'computer'
});
// 要实现的结果如下:
app1.data.name // 你访问了 name
app.data.age = 100;  // 你设置了 age,新的值为100
app2.data.university // 你访问了 university
app2.data.major = 'science'  // 你设置了major,新的值为 science

请实现这样的一个 Observer,要求如下:

  1. 传入参数只考虑对象,不考虑数组。
  2. new Observer 返回一个对象,其 data 属性要能够访问到传递进去的对象。
  3. 通过 data 访问属性和设置属性的时候,均能打印出右侧对应的信息。

监听属性的变化,有两种方式:

  1. 采用ES5中的 defineProperty,设置 get 和 set 函数,重新定义读取和赋值的方式
  2. 采用ES6中的 proxy,对目标对象进行”拦截”

方式一:

Object.defineProperty(obj, prop, descriptor) 定义对象的属性。 Object.defineProperty 可设置的属性如下:

  • configurable:能否使用 delete、能否修改属性特性、能否修改访问器属性,false 为不可重新定义。默认为true。
  • enumerable:对象属性是否可通过 for-in 循环遍历或在 Object.keys 中列举,默认为true。
  • writable:对象属性是否可修改,默认为true。
  • value:对象属性的默认值,默认值为 undefined。

注意: configurable, enumerable, writable 特性默认值根据对象定义方法不同而不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 直接在对象上定义属性,这些特性默认值为true
var obj = {};
obj.name = 'ruirui';
console.log(Object.getOwnPropertyDescriptor(obj, 'name'));
// Object {value: "ruirui", writable: true, enumerable: true, configurable: true}

// 调用Object.defineProperty()方法,不指定值的时候,默认为false
var obj = {};
Object.defineProperty(obj, 'name', {
value: 'ruirui'
});
console.log(Object.getOwnPropertyDescriptor(obj, 'name'))
// Object {value: "ruirui", writable: false, enumerable: false, configurable: false}


// 在本例中,可以定义configurable、enumerable,默认为false。 但是如果定义了set或get方法中的任何一个,就不能再设置writable,即使false也不可以。

接着获取对象属性:

方法一:使用 Object.keys(obj),该方法返回一个数组,数组里是 obj 可被枚举的所有属性,接着对数组进行 forEach 遍历。
方法二:使用 for in 获取所有属性,接着用 obj.hasOwnProperty(key) 对属性进行判断过滤。

接着就可以自定义get和set函数啦!

在这过程中犯了一个低级错误:get 函数 return data[key],导致 get 函数返回时又触发了 get 函数,陷入死循环。

最终代码(Demo):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Observer(data) {
this.data = data;
this.getset(data);
}
Observer.prototype.getset = function(data) {
for(var key in data) {
var val = data[key];
if(data.hasOwnProperty(key)) {
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
//writable: true, //这里不能定义此属性,报错:Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object>
get: function () {
console.log('你访问了' + key);
return val;
},
set: function (newval) {
console.log('你设置了' + val + ',新的值为' + newval);
val = newval;
}
})
}
}
}

方式二:

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

最终代码(Demo):

function Observer (data) {
  return new Proxy(data, {
    get: function (target, propKey) {
      if (propKey in target) {
        console.log('你访问了' + propKey);
        return target[propKey];
      } else {
        console.log("Property \"" + propKey + "\" does not exist.");
      }
    },
    set: function (target, propKey, value) {
      console.log('你设置了' + propKey + ',新的值为' + value);
      target[propKey] = value;
    }
  })
}

后续补充:defineProperty VS proxy:

vue 文档中提到:

由于 JavaScript 的限制, Vue 不能检测以下变动的数组:
当你利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如: vm.items.length = newLength

vue 不能检测到数组的变动,从而无法更新视图,vue 监听数据变动使用的上述 Object.defineProperty,如果传入的是一个数组,通过下标访问数组,下标作为属性,可以看到是能正常调用 get 和 set 方法的,是可以被监听的,但是新增一个元素,修改数组长度,以及删除元素都不会触发监听事件。

1
2
3
4
5
6
7
8
9
let data = ['1','2','3'];
let app1 = new Observer(data);
console.log(app1.data[1]); // 你访问了2
app1.data[1] = 3; // 你设置了3,新的值为3
app1.length = 5;
console.log(app1.data[4]); // undefined
app1.data[4] = 4;
console.log(app1.data[4]); // 4
app1.data[5] = 9;

所以,vue 中重写了对数组的监测。这是 defineProperty 的第一个缺陷,第二个缺陷是,只能劫持对象的属性,所以对深层对象的属性需要深度遍历。这就引出了 Proxy 的优点:

  • Proxy 可以直接监听对象而非属性。返回的是新对象,可以操作新的对象。
  • Proxy 可以直接监听数组的变化。

Vue3.0 也将放弃Object.defineProperty,改用性能更好的Proxy。