读懂Vue3响应式原理的前提(Proxy,Reflect,Set,Map,WeakMap)
一、Proxy
Proxy(代理):在目标对象之前架设一层拦截,可以对外界的访问进行改写
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
var proxy = new Proxy(target, handler);
target:要拦截的目标对象
handler:用来定制拦截行为。handler可拦截的行为有十几种 ,这里只介绍最常用的几种
1. get 捕获器
作用:拦截对象属性的读取
1var person = {
2 name: "张三"
3};
4var proxy = new Proxy(person, {
5 get: function (target, key) {
6 if (key in target) {
7 return target[key];
8 }else{
9 return '无结果'
10 }
11 }
12});
13console.log(proxy.name) //张三
14console.log(proxy.age) //无结果
15
在此之前,当访问一个不存在的属性时,返回的结果为undefined。但我们可以使用Proxy的get进行改写,当没有结果时,返回一个无结果
2. set 捕获器
作用:拦截对象的属性赋值操作
1let person = new Proxy({}, {
2 set: function (target, key, value) {
3 /* 如果年龄不是整数 */
4 if (key === 'age' && !Number.isInteger(value)) {
5 throw new TypeError('The age is not an integer')
6 }
7 target[key] = value
8 return true // 表示成功
9 }
10})
11person.age = 100
12console.log(person.age) // 100
13person.age = 'young' // 抛出异常: Uncaught TypeError: The age is not an integer
14
在set中,我们可以在赋值之前进行表单校验等操作
3. has 捕获器
作用:拦截判断target对象中是否含有某个属性的操作
1var person = {
2 name: "张三"
3};
4var proxy = new Proxy(person, {
5 has: function (target, key) {
6 console.log('正在进行in操作')
7 return key in target;
8 }
9});
10
11'name' in proxy
12
4. deleteProperty 捕获器
作用:拦截删除target对象属性的操作
1var person = {
2 name: "张三"
3};
4var proxy = new Proxy(person, {
5 deleteProperty : function (target, key) {
6 if (key === 'name') {
7 throw new Error(`name属性不能被删除`);
8 }
9 return delete target[key]
10 }
11});
12
13delete proxy.name
14
二、Reflect
Reflect 是一个内置对象,它提供了一系列用于操作对象的方法,不要直接实例化(new)使用。
Reflect 将 Object 对象的一些明显属于语言内部的方法(in、delete),放到Reflect对象上(Reflect.get、Reflect.set)。现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。
与Proxy搭配使用简单示例:
1let obj = {
2 a: 1,
3 b: 2
4 };
5
6 let proxy = new Proxy(obj, {
7 //receiver为代理后的对象
8 get(target, key, receiver) {
9 console.log("监听的key", key);
10 //等同于target[key],
11 return Reflect.get(target, key, receiver);
12 },
13 set(target, key, value, receiver) {
14 console.log("触发set");
15 //等同于target[key] = value;
16 Reflect.set(target, key, value, receiver);
17 },
18 deleteProperty(target, key) {
19 console.log("监听删除");
20 //等同于 delete target[key]
21 Reflect.deleteProperty(target, key);
22 //如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除
23 return true;
24 },
25 has(target, key) {
26 console.log("监听has");
27 //等同于 key in target
28 return Reflect.has(target, key);
29 },
30 });
31
32 proxy.a; //监听的key a
33
34 proxy.a = 4; //触发set
35
36 delete proxy.b; //监听删除
37
38 "a" in proxy; //监听has
39
三、Set
1. 特点
-
类似于数组,但成员的值都是唯一的
-
会比较类型,5不等于 ' 5 '
-
null,undefined,NaN不会过滤
-
本身是一个构造函数,需使用new进行实例化
2. 使用情景
- 数组去重
1[...new Set([2, 3, 5, 4, 5, 2, 2])] // [2 3 5 4]
2
- 字符串去重
1[...new Set('ababbc')].join('') // "abc"
2
3. 属性和方法
add:增加
delete:删除
has:判断是否存在
clear:清除所有成员
size:返回成员总数
1let s = new Set()
2
3s.add(1).add(2).add(2) //{1,2}
4
5s.delete(2) //{1}
6
7s.has(1) //true
8
9s.clear() //{}
10
11s.size //0 注意size是属性,不是方法的调用
12
4. 遍历
1let set = new Set([1, 2, 3]);
2//使用forEach进行遍历
3set.forEach((value) => console.log(value))
4// 1
5// 2
6// 3
7
四、Map
1. 特点
-
类似于对象,但属性不限于字符串
-
只有引用地址一样,Map结构才将其视为同一个键
1const map = new Map();
2
3map.set(['a'], 555);
4map.get(['a']) // undefined
5
上面看起来是同一个键,但['a']指向不同的内存地址,所有取值为undefined。应将数组先赋值给变量
1const map = new Map();
2const a = ["a"]
3map.set(a, 555);
4map.get(a) // 555
5
2. 属性和方法
set:添加
get:获取key对应的键值
delete:删除
has:判断是否存在
clear:清除所有成员
size:返回成员总数
1const m = new Map();
2const o = {p: 'Hello World'};
3
4m.set(o, 'content')
5m.get(o) // "content"
6
7m.has(o) // true
8m.delete(o) // true
9m.has(o) // false
10m.size //0
11
3. 遍历
可以通过forEach和for...of两种方式遍历Map数据结构,获取key与对应的value
1//注意:value在前
2map.forEach(function (value, key) {
3 console.log(key, value);
4});
5
6for (let o of map) {
7 console.log(o) //[key,value]
8}
9
map.keys():返回键名的遍历器
map.values():返回键值的遍历器
五、WeakMap
1. 类似于Map,但键名只能是对象,不接受其他类型的值作为键名
1const map = new WeakMap();
2map.set(1, 2) // Uncaught TypeError: Invalid value used as weak map key
3
2. 键名所指向的对象,不计入垃圾回收机制
先看Map存储的数据结构,新建map.js
1function usageSize() {
2 //获取当前进程的堆内存使用量
3 const used = process.memoryUsage().heapUsed;
4 return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
5}
6
7global.gc(); //手动清内存,需配置启动参数 --expose-gc
8console.log(usageSize()); // ≈ 3.15M
9
10let arr = new Array(10 * 1024 * 1024);
11const map = new Map();
12
13map.set(arr, 1);
14global.gc();
15console.log(usageSize()); // ≈ 83.29M
16
17arr = null;
18global.gc();
19console.log(usageSize()); // ≈ 83.29M
20
运行node --expose-gc map.js,得到打印结果,最后两次打印结果相似,说明即使arr为null,但Map中的{arr:1}并没有被内存回收。
如果arr想被垃圾回收,我们还需手动清除Map中的arr,修改map.js
1//在arr = null前新增
2map.delete(arr)
3global.gc();
4console.log(usageSize()); // ≈ 83.29M
5
6//... arr = null;
7
运行node --expose-gc map.js,得到打印结果
13.15M
283.29M
383.29M
43.29M //arr占用的内存被释放
5
也可使用WeakMap来保存,新建weakMap.js
1function usageSize() {
2 //获取当前进程的堆内存使用量
3 const used = process.memoryUsage().heapUsed;
4 return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
5}
6
7global.gc(); //手动清内存,需配置启动参数 --expose-gc
8console.log(usageSize()); // ≈ 3.15M
9
10let arr = new Array(10 * 1024 * 1024);
11const map = new WeakMap();
12
13map.set(arr, 1);
14global.gc();
15console.log(usageSize()); // ≈ 83.29M
16
17arr = null;
18global.gc();
19console.log(usageSize()); // ≈ 3.29M
20
运行node --expose-gc weakMap.js,得到打印结果,可以看到arr为null时,arr所占内存全部被释放。
正由于WeakMap的弱引用,WeakMap 的 key 是不可枚举的。因为key可能会随时被回收
3. 弱引用的只是键名,而不是键值
修改weakMap.js,将arr保存到value上
1function usageSize() {
2 //获取当前进程的堆内存使用量
3 const used = process.memoryUsage().heapUsed;
4 return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
5}
6
7global.gc(); //手动清内存,需配置启动参数 --expose-gc
8console.log(usageSize()); // ≈ 3.15M
9
10let arr = new Array(10 * 1024 * 1024);
11const map = new WeakMap();
12let key = {key:1}
13
14//将arr保存到value上
15map.set(key, arr);
16global.gc();
17console.log(usageSize()); // ≈ 83.29M
18
19arr = null;
20console.log(map.get(key)) // <10485760 empty items> 依旧能取到值
21global.gc();
22console.log(usageSize()); // ≈ 83.3M
23
运行node --expose-gc weakMap.js,根据打印结果可以看到arr为null时,map.get依旧能取到值,内存也没有被释放。说明弱引用的只是键名,而不是键值。
六、结尾
参考文章: