本文讨论如何监听对象中所有属性的读和写操作,以及对于数组的劫持特殊处理,本文将从侧面来介绍 Vue2.X版本中响应式数据监听的原理。本文将用到 Object.defineProperty方法,该方法以及[getter 和 setter] 方法的具体使用方式,可以参考另一篇博客文章
对象劫持
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
 function isObject(o) {
return typeof o === "object" && o != null;
}
/*核心函数:通过Object.defineProperty方法实现劫持*/
function defineReactive(data, key, value) {
/* 递归调用:解决value也是对象的情况 */
observe(value);
Object.defineProperty(data, key, {
get() {
console.log(`${key}--读`)
return value;
},
set(newValue) {
console.log(`${key}--写`)
/* 如果数据的值没有改变那么就直接返回 */
if (value === newValue) return;
/* 如果设置的新数据是对象,那么也应该进行监听 */
observe(newValue);
value = newValue;
}
})
}
/* Observer 类(构造函数) */
class Observer {
constructor(val) {
this.walk(val)
}
walk(data) {
/* 获取当前对象所有可枚举的 key */
let keys = Object.keys(data);

/* 遍历所有的 keys,通过 Object.defineProperty 给所有的 key 都添加 getter和 setter */
keys.forEach(key => defineReactive(data, key, data[key]));
}
}

function observe(o) {
if (!isObject(o)) return; /* 排除对象的情况 */
return new Observer(o); /* 获取Observer的实例 */
}

/* 测试数据 */
let data = {
person: {
name: "zs",
age: 18
}
};
observe(data);

上面代码同的核心方法是defineReactive函数,在该函数的内部我们通过Object.defineProperty方法实现了对对象中属性的读(get)和写(set)操作的监听。Observer类用于构建 observe实例对象,该实例的walk方法通过遍历的方式为对象中所有的属性都实现了getter 和 setter 方法

接下来,我们给出一组测试数据并贴出对应的显示结果。

通过对代码的研究和对数据的测试,我们验证了上面代码基本上能够完成对对象数据读写操作的监听,但仍然存在一些不足。

① 如果是新增加属性,那么则无法监听。
② 如果是数组的结构,那么也无法监听。

数组劫持
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
 function isObject(o) {
return typeof o === "object" && o != null;
}

function defineReactive(data, key, value) {
/* 递归调用:解决value也是对象的情况 */
observe(value);
Object.defineProperty(data, key, {
get() {
console.log(`${key}--读`)
return value;
},
set(newValue) {
console.log(`${key}--写`)
/* 如果数据的值没有改变那么就直接返回 */
if (value === newValue) return;
/* 如果设置的新数据是对象,那么也应该进行监听 */
observe(newValue);
value = newValue;
}
})
}

/* 获取数组原型的方法 */
let oldArrayMethods = Array.prototype;
/* 把oldArrayMethods作为原型对象创建一个新的空的对象 */
let newArrayMethods = Object.create(oldArrayMethods);
/* 整理数组中需要重写的方法 */
let methods = ["pop", "push", "shift", "unshift", "sort", "reverse", "splice"];
methods.forEach(method => {
newArrayMethods[method] = function(...args) {
let __ob__ = this.__ob__;
let result = oldArrayMethods[method].apply(this, args);
console.log(`监听到${method}方法`, this, args);

/* 注意:新添加的数据可能是对象也需要监听读写操作 */
/* 1.先获取新添加的数据参数 */
let insetData;
switch (method) {
case "push":
case "unshift":
insetData = args;
break;
case "splice":
insetData = args.slice(2);
default:
break;
}
console.log("insetData", insetData);

/* 2.对新添加的数据进行监听 */
if (insetData) __ob__.observerArr(insetData);

return result;
}
})

/* Observer 类(构造函数) */
class Observer {
constructor(val) {
/* 区分对象和数组的情况 */
if (Array.isArray(val)) {
/* 给当前的对象定义__ob__属性,该属性指向的是自己 */
Object.defineProperty(val, "__ob__", {
configurable: false,
enumerable: false,
value: this
})
/* 重写数组的原型方法,在这些重写的方法内部进行监听 */
Reflect.setPrototypeOf(val, newArrayMethods);
this.observerArr(val);
} else {
this.walk(val)
}
}
walk(data) {
/* 获取当前对象所有可枚举的 key */
let keys = Object.keys(data);

/* 遍历所有的 keys,通过 Object.defineProperty 给所有的 key 都添加 getter和 setter */
keys.forEach(key => defineReactive(data, key, data[key]));
}
observerArr(arr) {
arr.forEach(item => observe(item));
}
}

function observe(o) {
if (!isObject(o)) return; /* 排除对象的情况 */
return new Observer(o); /* 获取Observer的实例 */
}

/* 测试数据 */
let data = {
person: {
name: "zs",
age: 18
},
friends: [{
name: "佩琪",
age: 3
}, {
name: "巧虎",
age: 5
}]
};
observe(data);

根据测试数据贴出对应的显示结果如下所示。

上述代码完成了对数组数据的读写监听(劫持),但仍然存在一些无法处理的情况,下面简单列出。

问题1:如果我们通过数组的下标来访问和修改数据,那么无法监听。
问题2:如果我们通过数组的 length 属性来操作(删除)数组,那么也无法监听。

上面的这些问题在 Vue 2.X 版本中主要通过 Vue.set() 或者是 vm.$set() 来进行实现。