知识点:
Object.defineProperty
Proxy
Reflect
介绍
依赖收集,是希望收集函数依赖的数据,当数据发生修改的时候,重新执行与其绑定的函数。实现依赖收集的基础就是监控数据修改。
监控数据修改
Object.defineProperty(obj, prop, descriptor)
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
Object.defineProperty
ECMAScript 5 ✔ 96.29% ◒ 2.68%
IE ✘ 5.5+ ✘ 8+⁴ ◒ 9+² ✔ 10+ Edge ✔ Firefox ◒ 2+ ◒ 4+¹ ✔ 21+ Chrome ◒ 4+ ◒ 19+¹ ✔ 23+ Safari
◒ 3.1+ ✔ 6+ Opera ◒ 9+ ◒ 12.1+¹ ✔ 15+
Object.defineProperty(obj, prop, descriptor)
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
参数
- obj 要在其上定义属性的对象。
- prop 要定义或修改的属性的名称。
- descriptor 将被定义或修改的属性描述符
value, enumerable, configurable, writable, set, get
。
返回值
- 被传递给函数的对象。
Object.defineProperty
可以精确添加一个新的属性,并对其设置。要对数据实现监控,我们需要理解属性描述符:
描述符 | 介绍 |
---|---|
value | 设置该属性的属性值 |
enumerable | 是否可以枚举 |
configurable | 是否可以操作 (表示对象的属性是否可以被删除,以及除 value 和 writable 特性外的其他特性是否可以被修改。) |
writable | 是否可写 |
get | 给 getter 提供方法的 |
set | 给 setter 提供方法的 |
如果一个描述符同时具有 (value / writable) 和 (get / set) 关键字,将会产生异常
在 VUE 中就是把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项,Vue 将遍历它的属性,用 Object.defineProperty
将它们转为 getter/setter。
示例
创建属性
1 | let people = {}; |
需要注意的是,描述对象(description)中的属性是具有继承性的,如果没有覆盖其 prototype
上的这些属性,可能存在风险;我们可以通过 Object.create(null)
创建一个空的对象的方式避免这个问题
1 | let people = {}; |
这些描述符都具有默认值:
当描述符中省略某些字段时,这些字段将使用它们的默认值。拥有布尔值的字段的默认值都是 false。value,get 和 set 字段的默认值为 undefined。一个没有 get/set/value/writable 定义的属性被称为“通用的”,并被“键入”为一个数据描述符。
-
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
50function PeopleGroup() {
var peoples = [];
var peoplesName = [];
function setPeoples(value) {
if (!value.name) {
console.log("The name cant empty");
return false;
}
if (peoplesName.includes(value.name)) {
console.log(value.name + " Is exist");
return false;
}
peoples.push(value);
peoplesName.push(value.name);
console.log(value.name + " 信息设置为 " + JSON.stringify(value));
return true;
}
Object.defineProperty(this, "people", {
get() {
return peoplesName;
},
set(val) {
setPeoples(val);
}
});
this.getPeoplesInfo = () => {
return peoples;
};
}
let peoples = new PeopleGroup();
peoples.people = {
name: "小明",
age: "12"
};
/* 小明 信息设置为 {"name":"小明","age":"12"} */
peoples.people = {
name: "小白",
age: "12"
};
/* 小白 信息设置为 {"name":"小白","age":"12"} */
peoples.people = {
name: "小明",
age: "12"
};
/* 小明 Is exist */
console.log(peoples.people);
/* ["小明", "小白"] */
console.log(JSON.stringify(peoples.getPeoplesInfo()));
/* [{"name":"小明","age":"12"},{"name":"小白","age":"12"}] */ -
Array
对于数组的修改比较特殊,通常我们会使用
push
方法来修改数组,如果需要对修改进行监控我们需要使用描述符value
重写push
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19let people = {
name: "小明",
friends: ["小白", "小红"]
};
Object.defineProperty(people.friends, "push", {
value(name, age) {
this[this.length] = arguments[0];
console.log(
"这里添加了一个新的朋友:" +
arguments[0] +
" 年龄:" +
arguments[1]
);
}
});
people.friends.push("小黑", 12);
// 这里添加了一个新的朋友:小黑 年龄:12注:示例中严重修改了
push
容易造成误解,不建议这样使用;
Proxy
new Proxy(target, handler)
使用描述对象(handle)来包装目标对象(target),用于定义基本操作的自定义行为。最终返回包装后的对象
示例:
1 | // 目的:当访问对象中不存在的属性名时,返回 false |
使用描述对象(handler) 还可以改写 set
行为,这种方式可以对添加的属性进行验证。
-
利用 Proxy 我们可以改写 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
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
60function PeopleGroup() {
var peoples = [];
var peoplesName = [];
const handler = {
set(obj, prop, value) {
if (prop == "people") {
if (!value.name) {
console.log("The name cant empty");
return false;
}
if (peoplesName.includes(value.name)) {
console.log(value.name + " Is exist");
return false;
}
peoples.push(value);
peoplesName.push(value.name);
console.log(
value.name + " 信息设置为 " + JSON.stringify(value)
);
} else {
obj[prop] = value;
// 添加了其它属性
}
},
get(obj, prop) {
if (prop == "people") {
return peoplesName;
} else {
return obj[prop];
}
}
};
let new_this = new Proxy(this, handler);
new_this.getPeoplesInfo = () => {
return peoples;
};
return new_this;
}
let peoples = new PeopleGroup();
peoples.people = {
name: "小明",
age: "12"
};
/* 小明 信息设置为 {"name":"小明","age":"12"} */
peoples.people = {
name: "小白",
age: "12"
};
/* 小白 信息设置为 {"name":"小白","age":"12"} */
peoples.people = {
name: "小明",
age: "12"
};
/* 小明 Is exist */
console.log(peoples.people);
/* ["小明", "小白"] */
console.log(JSON.stringify(peoples.getPeoplesInfo()));
/* [{"name":"小明","age":"12"},{"name":"小白","age":"12"}] */ -
Array
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19let people = {
name: "小明",
friends: ["小白", "小红"]
};
const handler = {
set(target, props, value) {
if (props === "length") return true;
console.log(JSON.stringify(target) + " 修改了 ");
target[props] = value;
console.log("修改结果:" + JSON.stringify(target));
return true;
}
};
var proxy_people_friends = new Proxy(people.friends, handler);
proxy_people_friends.push("小黑");
// ["小白","小红"] 修改了
// 修改结果:["小白","小红","小黑"]注:这里排除了
length
是因为使用push
的时候不仅会修改people.friends
还有修改其length
属性,会触发两次行为
使用 Proxy 存在一个问题 – 我们如何在劫持的行为中调用 target 本身的行为,比如:
1 | let people = { |
这里我们希望获得的是代理后的 name
,如何操作呢?下面是操作示例:
1 | let people = { |
我们再看个示例:
1 | let people = { |
可以看到如果继续使用,receiver
获取代理后的属性,就会出现重复调用,这个时候我们就需要使用 Reflect
来解决这个问题了
Reflect
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与处理器对象(handler)的方法相同。Reflect 的所有属性和方法都是静态的(就像 Math 对象)。
-
Reflect 相对于 Proxy 来说,提供了操作被代理对象的行为:
Reflect.set()
Reflect.set(target, propertyKey, value[, receiver])
拦截对象(target)设置值(value)的属性(propertyKey)。最终将会返回一个
boolean
表示是否成功。receiver 表示直接操作的对象示例:
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
31let people = {
get info() {
return this.name + "今年:" + this.age;
},
name: "小明",
age: 12
};
console.log(people.info); // 小明今年:12
let handle = {
get(target, key, receiver) {
if (key === "age") return 22;
console.log("Reflect:", Reflect.get(target, key, receiver));
console.log("target[key]:", target[key]);
/* :two: console.log("receiver[key]:", receiver[key]); */
return target[key];
}
};
let proxyPeople = new Proxy(people, handle);
/* :one: */
console.log(proxyPeople.info);
// Reflect: 小明
// target[key]: 小明
// Reflect: 小明今年:22
// target[key]: 小明今年:12
// 小明今年:12- 1️⃣ 这里使用的是
proxyPeople.info
直接操作的对象是proxyPeople
- 2️⃣ 如果这里对 receiver 进行操作,就会重复调用 get 变成死循环
可以看出使用 Reflect 可以使得在被代理的对象上使用被代理的方法;
- 1️⃣ 这里使用的是
-
Reflect 使我们对于函数的操作变成了函数操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let people = {
get info() {
return this.name + "今年:" + this.age;
},
name: "小明",
age: 12
};
let handle = {
get(target, key, receiver) {
if (key === "name") return "小白";
return Reflect.get(target, key, receiver);
}
};
let proxy_people = new Proxy(people, handle);
console.log(proxy_people.info); // 小白今年:12 -
更好的返回值
使用 Reflect 可以的到更好的返回值:true / false -
更可靠的 apply
我们通常使用fn.apply(obj, args)
的方式来绑定函数,但是apply
可能会被修改。这个时候我们可以使用Function.prototype.apply.call(f, obj, args)
来解决问题,有了 Reflect 后我们可以更加方便的解决这个问题:Reflect.apply(obj, args)
进阶
Proxy
new Proxy(target, handler)
handler 可以拦截的方法有:
handler.has()
:在判断代理对象是否拥有某个属性时触发该操作,比如在执行 “foo” in proxy 时。handler.get()
:在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。handler.set()
:在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。handler.defineProperty()
:在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, “foo”, {}) 时handler.deleteProperty()
:在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo 时。handler.construct()
:在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行 new proxy() 时。handler.ownKeys()
:在获取代理对象的所有属性键时触发该操作,比如在执行 Object.getOwnPropertyNames(proxy) 时。handler.apply()
:当目标对象为函数,且被调用时触发。handler.getPrototypeOf()
:在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。handler.setPrototypeOf()
:在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时。handler.getOwnPropertyDescriptor()
:在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, “foo”) 时。handler.isExtensible()
:在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时。handler.preventExtensions()
:在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时。
Reflect
Reflect 能够代理的方法和 handle 相同
实践
实现观察者模式
1 | let people = { |
使用 proxy & reflect 实现 TODO_List
实现依赖收集
1 | // 实现依赖收集 |
-
使用 Proxy 的依赖收集
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138// 实现依赖收集
class Dependence_manager {
constructor() {
// 存放依赖项
this.dependence_array = new Set();
}
// 添加依赖
depend(key) {
if (Dependence_manager.target) {
this.dependence_array.add({
key: key,
target: Dependence_manager.target
});
}
}
// 添加发布行为
notify(key) {
this.dependence_array.forEach(function(dependence) {
if (dependence.key == key) {
dependence.target();
}
});
}
}
class Observable {
constructor(obj) {
return this.define_reactive(obj);
}
define_reactive(obj) {
const dependence_manager = new Dependence_manager();
const handler = {
get(target, key, receiver) {
dependence_manager.depend(key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log(`我的${key}属性被修改为${value}了!`);
Reflect.set(target, key, value, receiver);
dependence_manager.notify(key);
}
};
return new Proxy(obj, handler);
}
}
class Watcher {
constructor(obj, key, cb, on_computed_update) {
this.obj = obj;
this.key = key;
this.cb = cb;
this.on_computed_update = on_computed_update;
return this.define_computed();
}
define_computed() {
const self = this;
let result;
Dependence_manager.target = () => {
result = self.cb();
this.on_computed_update(result);
};
result = self.cb();
Dependence_manager.target = null;
return new Proxy(self.obj, {
get(target, key, receiver) {
console.log("2");
if (key === self.key) {
return result;
} else {
return Reflect.get(target, key, receiver);
}
},
set(target, key, value, receiver) {
if (key === self.key) {
console.error("计算属性无法被赋值!");
} else {
return Reflect.set(target, key, value, receiver);
}
}
});
}
}
let observable_people = new Observable({
name: "小明",
age: 22,
job: "student",
info: ""
});
let observable_info_people = new Watcher(
observable_people,
"info",
function() {
let info =
observable_people.name + "今年:" + observable_people.age;
console.log("info 发生了修改:", info);
return info;
},
function(new_value) {
console.log("complete:", new_value);
}
);
let observable_job_people = new Watcher(
observable_people,
"age",
function() {
return observable_people.age < 18 ? "student" : "programmer";
},
function(new_value) {
console.log(observable_people.name + "的职业是:", new_value);
}
);
observable_people.name = "小白";
observable_people.age = 10;
observable_people.age = 15;
observable_people.age = 18;
observable_people.age = 20;
// info 发生了修改: 小明今年:22
// 我的name属性被修改为小白了!
// info 发生了修改: 小白今年:22
// complete: 小白今年:22
// 我的age属性被修改为10了!
// info 发生了修改: 小白今年:10
// complete: 小白今年:10
// 小白的职业是: student
// 我的age属性被修改为15了!
// info 发生了修改: 小白今年:15
// complete: 小白今年:15
// 小白的职业是: student
// 我的age属性被修改为18了!
// info 发生了修改: 小白今年:18注:这里可以看到,每一次修改都会执行回调函数,这会代指在实际使用中会造成性能损耗或者页面重复刷新页面的问题
-
使用异步优化
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150// 实现依赖收集
class Dependence_manager {
constructor() {
// 存放依赖项
this.dependence_array = new Set();
}
// 添加依赖
depend(key) {
if (Dependence_manager.target) {
this.dependence_array.add({
key: key,
target: Dependence_manager.target
});
Dependence_manager.target = null;
}
}
// 添加发布行为
async notify(key) {
this.dependence_array.forEach(function(dependence) {
if (dependence.key == key) {
dependence.target(key);
}
});
await Dependence_manager.target_array.clear();
}
}
Dependence_manager.target = null;
Dependence_manager.target_array = new Set();
class Observable {
constructor(obj) {
return this.define_reactive(obj);
}
define_reactive(obj) {
const dependence_manager = new Dependence_manager();
const handler = {
get(target, key, receiver) {
dependence_manager.depend(key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log(`我的${key}属性被修改为${value}了!`);
Reflect.set(target, key, value, receiver);
dependence_manager.notify(key);
}
};
return new Proxy(obj, handler);
}
}
class Watcher {
constructor(obj, key, cb, on_computed_update) {
this.obj = obj;
this.key = key;
this.cb = cb;
this.on_computed_update = on_computed_update;
return this.define_computed();
}
define_computed() {
const self = this;
let result;
Dependence_manager.target = async key => {
await console.log("watch");
// console.log(Dependence_manager.target_array.keys(), key);
if (!Dependence_manager.target_array.has(key)) {
Dependence_manager.target_array.add(key);
result = self.cb();
self.on_computed_update(result);
}
};
result = self.cb();
return new Proxy(self.obj, {
get(target, key, receiver) {
if (key === self.key) {
return result;
} else {
return Reflect.get(target, key, receiver);
}
},
set(target, key, value, receiver) {
if (key === self.key) {
console.error("计算属性无法被赋值!");
} else {
return Reflect.set(target, key, value, receiver);
}
}
});
}
}
let observable_people = new Observable({
name: "小明",
age: 22,
job: "student",
info: ""
});
let observable_info_people = new Watcher(
observable_people,
"info",
function() {
let info =
observable_people.name + "今年:" + observable_people.age;
console.log("callback:", info);
return info;
},
function(new_value) {
console.log("complete:", new_value);
}
);
let observable_job_people = new Watcher(
observable_people,
"job",
function() {
return observable_people.age < 18 ? "student" : "programmer";
},
function(new_value) {
console.log(observable_people.name + "的职业是:", new_value);
}
);
observable_people.name = "小白";
observable_people.age = 10;
observable_people.age = 15;
observable_people.age = 18;
observable_people.age = 20;
// callback: 小明今年:22
// 我的name属性被修改为小白了!
// watch
// 我的age属性被修改为10了!
// watch
// 我的age属性被修改为15了!
// watch
// 我的age属性被修改为18了!
// watch
// 我的age属性被修改为20了!
// watch
// callback: 小白今年:20
// complete: 小白今年:20
// 小白的职业是: programmer
参考文章:
- 实战解析依赖收集
- 深入实践 ES6 Proxy & Reflect
- MDN
- TechBridge 技術共筆部落格