1. 1. Object.defineProperty
  2. 2. Proxy
  3. 3. 完成

本文是Vue双向绑定的前置篇。我们都知道Vue是通过数据劫持来实现双向绑定的,那么什么是数据劫持?数据劫持是如何进行的?要弄明白这两个问题,我们首先要知道我们一般情况是怎么定义对象,怎么修改对象的,先看下简单的对象定义与修改;

1
2
3
4
const obj = {name: '张三'};
console.log(obj.name);
obj.name = '李四';
console.log(obj.name);

上面的代码中我们 console.log 打印 obj.name 的时候,是不是相当于先获取 obj.name 的值,然后再打印,那么获取的时候,你知道吗?你不知道;我们修改 obj.name 的值为 李四 的时候,你知道吗?你不知道;那么数据劫持是什么,数据劫持就是当你要取 obj.name 的值的时候通知我一下,你修改 obj.name 的时候也通知我一下,最好可以,你获取设置值的时候由我来返给你,而不是你直接操作;

所以想要实现这个目的,我们就需要用到一个API Object.defineProperty 或者 Proxy ,也是我们本文的主旨;

Object.defineProperty

Object.definePropertyVue 2.x 实现数据劫持的主要API,它也是ES5原生的一个API;语法 Object.defineProperty(obj, prop, descriptor) ,它接收三个参数,obj 原始对象,即你要修改或者获取的值属于那个对象; prop 要修改或获取的obj对象的key; descriptor 属性描述符,即可以设置这个对象的这个值,是否可枚举,是否只读等描述信息;

如此,那我们就把上方的代码,再加几个属性,然后用 Object.defineProperty 改写一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const defineReactive = (obj,key,value) => {
Object.defineProperty(obj,key, {
// 更多描述信息可查看MDN文档 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
// 可枚举
enumerable: true,
// 可配置
configurable: true,
// 获取这个对象的这个属性时调用这个函数
get() {
console.log('你正在获取', key, '属性', '它的值是',value,'我将把它的值返回给你');
return value;
},
// 修改这个对象的这个属性时调用这个函数
set(newValue) {
console.log('你正在修改', key, '属性的值', '你要将其改为', newValue, '我来改');
value = newValue;
}
})
}
const obj = {name: '霸宋', age: 18, sex: '男'};
Object.keys(obj).forEach(key => defineReactive(obj,key,obj[key]));

如此我们就用 Object.defineProperty 简单实现了数据劫持(上述代码可以粘贴到浏览器控制台运行),甚至你可以把 get 的返回写个其他值, set 的修改写成其他值,那么你再获取和修改,就是你写的值了,玩的时候可以用,真的做工具或者业务用,根据需求写就行。

但是上方还有点不完美,因为属性的值还可以是对象,而 Object.defineProperty 只能代理一层,对于属性的属性就不能劫持了,所以我们可以把上方代码再修改完善下,然后加个递归,如果属性的值还是对象我们再代理一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const observe = obj => {
// 如果obj不存在 或者 不是对象就直接返回
if(!obj || obj.constructor.name !== 'Object') return;
Object.keys(obj).forEach(key => defineReactive(obj,key,obj[key]));
}
const defineReactive = (obj,key,value) => {
observe(value);
Object.defineProperty(obj,key, {
// 更多描述信息可查看MDN文档 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
// 可枚举
enumerable: true,
// 可配置
configurable: true,
// 获取这个对象的这个属性时调用这个函数
get() {
console.log('你正在获取', key, '属性', '它的值是',value,'我将把它的值返回给你');
return value;
},
// 修改这个对象的这个属性时调用这个函数
set(newValue) {
console.log('你正在修改', key, '属性的值', '你要将其改为', newValue, '我来改');
value = newValue;
}
})
}
const obj = {name: '霸宋', age: 18, sex: '男', other: {father: '宋爸', mother: '宋妈'}};
observe(obj);

至此,使用 Object.defineProperty 做数据劫持基本完成了,但有四点需要注意;

  • Object.defineProperty 只能代理对象,不能代理字符串,数组,number等其他对象;
  • Object.defineProperty 只能代理你设置了的key,你没设置代理的不会被代理,也就是说,你代理之后,又给元对象加了新属性,不会被代理;
  • Object.defineProperty 需要在对象定义完成后就去代理,如果定义后先获取修改,再代理,只会对代理之后的获取修改起作用,代理之前的数据获取修改拦截不到
  • 不使用 Object.defineProperty 代理对象,不表示你获取修改对象的属性时不通过 getset 拦截器,只是不代理对象属性的修改获取是隐式的。

Proxy

上文我们已经表述了什么是数据劫持,以及怎样进行数据劫持,所以接下来我们所要做的只是把 Object.defineProperty 方法换成 Proxy 就可以。

首先我们看下 Proxy 的语法 new Proxy(target, handler) ;可以看到 Proxy 是一个构造函数,使用它的时候我们需要对其进行实例化,然后它接收两个参数 target 要被代理的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理),handler 一个对象,其属性是当执行一个操作时定义代理的行为的函数。

然后我们把上方的代码用 Proxy 改写一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let obj = {name: '霸宋', age: 18, sex: '男', other: {father: '宋爸', mother: '宋妈'}};
const observe = obj => {
// 如果obj不存在 或者 不是对象就直接返回可以代理数组
if(!obj || typeof obj !== 'object') return;
// 如果obj的属性还是对象或数组就将其过滤出来,然后递归劫持
Object.keys(obj).filter(key => obj[key].constructor.name === 'Object' || obj[key].constructor.name === 'Array').forEach(key => obj[key] = observe(obj[key]))
return new Proxy(obj, {
get(target,key) {
console.log('你正在获取', key, '属性', '它的值是',target[key],'我将把它的值返回给你');
return target[key];
},
// 修改这个对象的这个属性时调用这个函数
set(target,key,newValue) {
console.log('你正在修改', key, '属性的值', '你要将其改为', newValue, '我来改');
target[key] = newValue;
}
})
}
obj = observe(obj);

至此使用 Proxy 进行数据劫持基本改写完成,递归的地方感觉不完美,但目的是实现了,不过需要关注 ProxyObject.defineProperty 的性能消耗,以及 Proxy 的兼容性问题,但不是本文关注点,也就不研究了。

完成