读懂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 入门

相关推荐
余生H6 分钟前
前端Python应用指南(二)深入Flask:理解Flask的应用结构与模块化设计
前端·后端·python·flask·全栈
outstanding木槿11 分钟前
JS中for循环里的ajax请求不数据
前端·javascript·react.js·ajax
酥饼~18 分钟前
html固定头和第一列简单例子
前端·javascript·html
一只不会编程的猫21 分钟前
高德地图自定义折线矢量图形
前端·vue.js·vue
所以经济危机就是没有新技术拉动增长了22 分钟前
二、javascript的进阶知识
开发语言·javascript·ecmascript
m0_7482509323 分钟前
html 通用错误页面
前端·html
来吧~32 分钟前
vue3使用video-player实现视频播放(可拖动视频窗口、调整大小)
前端·vue.js·音视频
Bubluu33 分钟前
浏览器点击视频裁剪当前帧,然后粘贴到页面
开发语言·javascript·音视频
鎈卟誃筅甡1 小时前
Vuex 的使用和原理详解
前端·javascript
呆呆小雅1 小时前
二、创建第一个VUE项目
前端·javascript·vue.js