任务说明地址
任务描述
这是“动态数据绑定”系列的第一题。
我之前经常使用 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,要求如下:
- 传入参数只考虑对象,不考虑数组。
- new Observer 返回一个对象,其 data 属性要能够访问到传递进去的对象。
- 通过 data 访问属性和设置属性的时候,均能打印出右侧对应的信息。
监听属性的变化,有两种方式:
- 采用ES5中的 defineProperty,设置 get 和 set 函数,重新定义读取和赋值的方式。
- 采用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 | // 直接在对象上定义属性,这些特性默认值为true |
接着获取对象属性:
方法一:使用 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
24function 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 | let data = ['1','2','3']; |
所以,vue 中重写了对数组的监测。这是 defineProperty 的第一个缺陷,第二个缺陷是,只能劫持对象的属性,所以对深层对象的属性需要深度遍历。这就引出了 Proxy 的优点:
- Proxy 可以直接监听对象而非属性。返回的是新对象,可以操作新的对象。
- Proxy 可以直接监听数组的变化。
Vue3.0 也将放弃Object.defineProperty,改用性能更好的Proxy。