Jansiel Notes

读懂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. 特点

  1. 类似于数组,但成员的值都是唯一的

  2. 会比较类型,5不等于 ' 5 '

  3. null,undefined,NaN不会过滤

  4. 本身是一个构造函数,需使用new进行实例化

2. 使用情景

  1. 数组去重
1[...new Set([2, 3, 5, 4, 5, 2, 2])] // [2 3 5 4]
2
  1. 字符串去重
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. 特点

  1. 类似于对象,但属性不限于字符串

  2. 只有引用地址一样,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依旧能取到值,内存也没有被释放。说明弱引用的只是键名,而不是键值。

六、结尾

参考文章:

  1. 你不知道的 WeakMap

  2. ECMAScript 6 入门