读懂Vue3响应式原理的前提(Proxy,Reflect,Set,Map,WeakMap)

一、Proxy

Proxy(代理):在目标对象之前架设一层拦截,可以对外界的访问进行改写

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

var proxy = new Proxy(target, handler);

target:要拦截的目标对象

handler:用来定制拦截行为。handler可拦截的行为有十几种 ,这里只介绍最常用的几种

1. get 捕获器

作用:拦截对象属性的读取

js 复制代码
var person = {
    name: "张三"
};
var proxy = new Proxy(person, {
    get: function (target, key) {
        if (key in target) {
            return target[key];
        }else{
            return '无结果'
        }
    }
});
console.log(proxy.name) //张三
console.log(proxy.age) //无结果

在此之前,当访问一个不存在的属性时,返回的结果为undefined。但我们可以使用Proxy的get进行改写,当没有结果时,返回一个无结果

2. set 捕获器

作用:拦截对象的属性赋值操作

js 复制代码
let person = new Proxy({}, {
    set: function (target, key, value) {
        /* 如果年龄不是整数 */
        if (key === 'age' && !Number.isInteger(value)) {
            throw new TypeError('The age is not an integer')
        }
        target[key] = value
        return true // 表示成功
    }
})
person.age = 100
console.log(person.age)  // 100
person.age = 'young'  // 抛出异常: Uncaught TypeError: The age is not an integer

在set中,我们可以在赋值之前进行表单校验等操作

3. has 捕获器

作用:拦截判断target对象中是否含有某个属性的操作

js 复制代码
var person = {
    name: "张三"
};
var proxy = new Proxy(person, {
    has: function (target, key) {
      console.log('正在进行in操作')
      return key in target;
    }
});

'name' in proxy

4. deleteProperty 捕获器

作用:拦截删除target对象属性的操作

js 复制代码
var person = {
    name: "张三"
};
var proxy = new Proxy(person, {
  deleteProperty : function (target, key) {
    if (key === 'name') {
    throw new Error(`name属性不能被删除`);
   }
    return delete target[key]
    }
});

delete proxy.name

二、Reflect

Reflect 是一个内置对象,它提供了一系列用于操作对象的方法,不要直接实例化(new)使用。

Reflect 将 Object 对象的一些明显属于语言内部的方法(in、delete),放到Reflect对象上(Reflect.get、Reflect.set)。现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。

与Proxy搭配使用简单示例:

js 复制代码
let obj = {
    a: 1,
    b: 2
  };
  
  let proxy = new Proxy(obj, {
    //receiver为代理后的对象
    get(target, key, receiver) {
      console.log("监听的key", key);
      //等同于target[key],
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      console.log("触发set");
      //等同于target[key] = value;
      Reflect.set(target, key, value, receiver);
    },
    deleteProperty(target, key) {
      console.log("监听删除");
      //等同于 delete target[key]
      Reflect.deleteProperty(target, key);
        //如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除
      return true;
    },
    has(target, key) {
      console.log("监听has");
      //等同于 key in target
      return Reflect.has(target, key);
    },
  });
  
  proxy.a; //监听的key a
  
  proxy.a = 4; //触发set
  
  delete proxy.b; //监听删除
  
  "a" in proxy; //监听has

三、Set

1. 特点

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

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

  3. null,undefined,NaN不会过滤

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

2. 使用情景

  1. 数组去重
js 复制代码
[...new Set([2, 3, 5, 4, 5, 2, 2])] // [2 3 5 4]
  1. 字符串去重
js 复制代码
[...new Set('ababbc')].join('') // "abc"

3. 属性和方法

add:增加

delete:删除

has:判断是否存在

clear:清除所有成员

size:返回成员总数

js 复制代码
let s = new Set()

s.add(1).add(2).add(2) //{1,2}

s.delete(2) //{1}

s.has(1) //true

s.clear() //{}

s.size //0 注意size是属性,不是方法的调用

4. 遍历

js 复制代码
let set = new Set([1, 2, 3]);
//使用forEach进行遍历
set.forEach((value) => console.log(value))
// 1 
// 2
// 3

四、Map

1. 特点

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

  2. 只有引用地址一样,Map结构才将其视为同一个键

js 复制代码
const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined

上面看起来是同一个键,但['a']指向不同的内存地址,所有取值为undefined。应将数组先赋值给变量

js 复制代码
const map = new Map();
const a = ["a"]
map.set(a, 555);
map.get(a) // 555

2. 属性和方法

set:添加

get:获取key对应的键值

delete:删除

has:判断是否存在

clear:清除所有成员

size:返回成员总数

js 复制代码
const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false
m.size //0

3. 遍历

可以通过forEach和for...of两种方式遍历Map数据结构,获取key与对应的value

js 复制代码
//注意:value在前
map.forEach(function (value, key) {
    console.log(key, value);
});

for (let o of map) {
    console.log(o) //[key,value]
}

map.keys():返回键名的遍历器

map.values():返回键值的遍历器

五、WeakMap

1. 类似于Map,但键名只能是对象,不接受其他类型的值作为键名

js 复制代码
const map = new WeakMap();
map.set(1, 2) // Uncaught TypeError: Invalid value used as weak map key

2. 键名所指向的对象,不计入垃圾回收机制

先看Map存储的数据结构,新建map.js

js 复制代码
function usageSize() {
  //获取当前进程的堆内存使用量  
  const used = process.memoryUsage().heapUsed;
  return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}

global.gc(); //手动清内存,需配置启动参数 --expose-gc
console.log(usageSize()); // ≈ 3.15M

let arr = new Array(10 * 1024 * 1024);
const map = new Map();

map.set(arr, 1);
global.gc();
console.log(usageSize()); // ≈ 83.29M

arr = null;
global.gc();
console.log(usageSize()); // ≈ 83.29M

运行node --expose-gc map.js,得到打印结果,最后两次打印结果相似,说明即使arr为null,但Map中的{arr:1}并没有被内存回收。

如果arr想被垃圾回收,我们还需手动清除Map中的arr,修改map.js

js 复制代码
//在arr = null前新增
map.delete(arr)
global.gc();
console.log(usageSize()); // ≈ 83.29M

//... arr = null;

运行node --expose-gc map.js,得到打印结果

js 复制代码
3.15M
83.29M
83.29M
3.29M  //arr占用的内存被释放

也可使用WeakMap来保存,新建weakMap.js

js 复制代码
function usageSize() {
  //获取当前进程的堆内存使用量
  const used = process.memoryUsage().heapUsed;
  return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}

global.gc(); //手动清内存,需配置启动参数 --expose-gc
console.log(usageSize()); // ≈ 3.15M

let arr = new Array(10 * 1024 * 1024);
const map = new WeakMap();

map.set(arr, 1);
global.gc();
console.log(usageSize()); // ≈ 83.29M

arr = null;
global.gc();
console.log(usageSize()); // ≈ 3.29M

运行node --expose-gc weakMap.js,得到打印结果,可以看到arr为null时,arr所占内存全部被释放。

正由于WeakMap的弱引用,WeakMap 的 key 是不可枚举的。因为key可能会随时被回收

3. 弱引用的只是键名,而不是键值

修改weakMap.js,将arr保存到value上

js 复制代码
function usageSize() {
  //获取当前进程的堆内存使用量
  const used = process.memoryUsage().heapUsed;
  return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}

global.gc(); //手动清内存,需配置启动参数 --expose-gc
console.log(usageSize()); // ≈ 3.15M

let arr = new Array(10 * 1024 * 1024);
const map = new WeakMap();
let key = {key:1}

//将arr保存到value上
map.set(key, arr);
global.gc();
console.log(usageSize()); // ≈ 83.29M

arr = null;
console.log(map.get(key)) // <10485760 empty items> 依旧能取到值
global.gc();
console.log(usageSize()); // ≈ 83.3M

运行node --expose-gc weakMap.js,根据打印结果可以看到arr为null时,map.get依旧能取到值,内存也没有被释放。说明弱引用的只是键名,而不是键值。

六、结尾

参考文章:

  1. 你不知道的 WeakMap

  2. ECMAScript 6 入门

相关推荐
落霞的思绪43 分钟前
CSS复习
前端·css
咖啡の猫3 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲5 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5816 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路6 小时前
GeoTools 读取影像元数据
前端
ssshooter6 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友6 小时前
【Node.js】什么是Node.js
javascript·后端·node.js
Jerry7 小时前
Jetpack Compose 中的状态
前端
dae bal8 小时前
关于RSA和AES加密
前端·vue.js
柳杉8 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化