download:Java 架构师成长直通车(40 周完结无密)
在读 Vue 3 响应式原理部分代码的过程中看到其在进行响应式处理的时候,为每个对象使用 WeakMap 创建了一个「缓存区」,代码如下:
// 注意上面这句代码!
const reactiveMap = new WeakMap();
// 核心进行劫持的方法 处理 get 和 set 的逻辑
const mutableHandlers = {
get,
set
}
function reactive(target: object) {
return createReactiveObject(target, mutableHandlers, reactiveMap);
}
/**
- @description 创建响应式对象
- @param {Object} target 需要被代理的目标对象
- @param {Function} baseHandlers 针对每种形式对应的不同处理函数
- @param {Object} proxyMap WeakMap 对象
*/
function createReactiveObject(target, baseHandlers, proxyMap) {
// 检测 target 是不是对象, 不是对象间接返回,不进行代理
if (!isObject(target)) {return target}
const existsProxy = proxyMap.get(target);
// 如果该对象已经被代理过了,则间接返回,不进行重复代理
if (existsProxy) {return existsProxy}
// 未被代理过,则创建代理对象
const proxy = new Proxy(target,baseHandlers);
// 缓存,避免重复代理,即避免 reactive(reactive(Object)) 的情况出现
proxyMap.set(target,proxy);
return proxy
}
从下面的代码可能看出,WeakMap 缓存区的作用就是用来防止对象被重复代理。
为什么 Vue 3 使用 WeakMap 来缓存代理对象?为什么不使用其余的形式来进行缓存,比方说 Map?
什么是 WeakMap
WeakMap 对象是一组键值对的会合,其中的键是 弱引用 的。其键必须是 对象,而值可能是任意的。
语法
new WeakMap([iterable])
Iterable 是一个数组(二元数组)或者其余可迭代的且其元素是键值对的对象。每个键值对会被加到新的 WeakMap 里。
方法
WeakMap 有四个方法:别离是 get、set、has、delete,上面咱们看一下其大抵的用法:
const wm1 = new WeakMap(),
wm2 = new WeakMap(),
wm3 = new WeakMap();
const o1 = {},
o2 = function() {},
o3 = window;
wm1.set(o1, 37);
wm1.set(o2, “azerty”);
wm2.set(o1, o2); // value 可能是任意值,包含一个对象或一个函数
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // 键和值可能是任意对象,以至另外一个 WeakMap 对象
wm1.get(o2); // “azerty”
wm2.get(o2); // undefined,wm2 中没有 o2 这个键
wm2.get(o3); // undefined,值就是 undefined
wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即使值是 undefined)
wm3.set(o1, 37);
wm3.get(o1); // 37
wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false
为什么要用 WeakMap 而不是 Map
在 JavaScript 里,map API 可能通过四个 API 方法共用两个数组(一个存放键, 一个存放值)来实现。这样在给这种 map 设置值时会同时将键和值增加到这两个数组的开端。从而使得键和值的索引在两个数组中绝对应。当从该 map 取值的时候,需要遍历所有的键,而后使用索引从存储值的数组中检索出相应的值。
但这样的实现会有两个很大的缺点,首先赋值和搜寻操作都是 O(n) 的工夫复杂度(n 是键值对的个数),因为这两个操作都需要遍历整个数组来进行匹配。
另外一个缺点是可能会导致 内存泄漏,因为数组会一直引用着每个键和值。这种引用使得 垃圾回收算法不能回收处理他们,即使没有其余任何引用存在了。
let jser = {name: “dachui”};
let array = [jser];
jser = null; // 覆盖引用
下面这段代码,咱们把一个对象放入到数组中,那么只需这个数组存在,那么这个对象也就存在,即使没有其余对该对象的引用。
let jser = {name: “dachui”};
let map = new Map();
map.set(jser, “”);
jser = null; // 覆盖引用
类似的,如果咱们使用对象作为惯例 Map 的键,那么当 Map 存在时,该对象也将存在。它会占用内存,并且不会被垃圾回收机制回收。
相比之下,原生的 WeakMap 持有的是每个键对象的 弱引用,这意味着在没有其余引用存在时垃圾回收能正确进行。
正是因为这样的弱引用,WeakMap 的 key 是不可枚举的 (没有方法能给出所有的 key)。如果 key 是可枚举的话,其列表将会受垃圾回收机制的影响,从而失去不必定的后果。因此,如果你想要这种类型对象的 key 值的列表,你应该使用 Map。
综上,咱们可能得出以下论断:WeakMap 的键所指向的对象,不计入垃圾回收机制。
所以,如果你要往对象上增加数据,又不想烦扰垃圾回收机制,就可能使用 WeakMap。
看到这里大家就应该知道了,Vue 3 之所以使用 WeakMap 来作为缓冲区就是为了能将 不再使用的数据进行正确的垃圾回收。
什么是弱引用
对于「弱引用」,维基百科给出了答案:
在计算机程序设计中,弱引用 与 强引用 绝对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用,则被认为是不可拜访(或弱可拜访)的,并因此 可能在任何时刻被回收。
为什么会出现弱引用
那么,为什么会出现弱引用呢?弱引用除了能解决上述问题之外还能解决什么问题呢?要想回答这些问题,咱们首先需要了解一下 V8 引擎是如何进行垃圾回收的。
对于 JSer 来说,内存的治理是主动的、有形的,这所有都归功于 V8 引擎在背地默默地帮咱们找到不需要使用的内存并进行清理。
那么,当咱们不再需要某个货色时会发生什么,V8 引擎又是如何发现并清理它的呢?
现在各大阅读器通常用采纳的垃圾回收有两种方法,一种是「引用计数」,另外一种就是「标记清除」。上面咱们来看一下:
标记清除
标记清除被称为 mark-and-sweep,它是基于 可达性 来判断对象是否存活的,它会定期执行以下「垃圾回收」步骤:
垃圾收集器找到所有的根,并标记(记住)它们。
而后它遍历并标记来自它们的所有引用。所有被遍历到的对象都会被记住,免得将来再次遍历到同一个对象。
……如此操作,直到所有可达的(从根部)引用都被拜访到。
没有被标记的对象都会被删除。
咱们还可能将这个过程设想成从根溢出一个巨大的油漆桶,它流经所有引用并标记所有可到达的对象,而后移除未标记的。
引用计数
引用计数形式最基本的状态就是让每个被治理的对象与一个引用计数器关联在一起,该计数器记录着该对象以后被引用的次数,每当创建一个新的引用指向该对象时其计数器就加 1,每当指向该对象的引用生效时计数器就减 1。当该计数器的值降到 0 就认为对象死亡。
区别
引用计数与基于「可达性」的标记清除的内存治理形式最大的区别就是,前者只需要 局部的信息,而后者需要 全局的信息。
在引用计数中每个计数器只记录了其对应对象的局部信息 —— 被引用的次数,而没有(也不需要)一份全局的对象图的生死信息。
因为只保护局部信息,所以不需要扫描全局对象图就可能识别并开释死对象。但也因为不足全局对象图信息,所以 无奈处理循环引用 的状况。
所以,更高级的引用计数实现会引入 弱引用 的概念来冲破某些已知的循环引用。
WeakMap 利用
存储 DOM 节点
WeakMap 利用的典型场合就是以 DOM 节点作为键名。上面是一个例子。
const myWeakmap = newWeakMap();
myWeakmap.set(
document.getElementById(‘logo’),
{timesClicked: 0},
);
document.getElementById(‘logo’).addEventListener(‘click’, () => {
const logoData = myWeakmap.get(document.getElementById(‘logo’));
logoData.timesClicked++;
}, false);
下面代码中,document.getElementById(‘logo’) 是一个 DOM 节点,每当发生 click 事件,就更新一下状态。咱们将这个状态作为值放在 WeakMap 里,对应的键就是这个节点对象。一旦这个 DOM 节点删除,该状态就会主动消失,不存在内存泄漏危险。
数据缓存
谜底就在谜面上,文章一结尾咱们提出的问题就是这里的答案。Vue 3 在实现响应式原理的时候就是使用了 WeakMap 来作为响应式对象的「缓存区」。
对于这一点用法也很简略,当咱们需要关联对象和数据,比如在不修改原有对象的情况下储存某些属性或者根据对象储存一些计算的值等,而又不想手动去治理这些内存问题的时候就可能使用 WeakMap。
部署类中的公有属性
WeakMap 的另一个用处是部署类中的公有属性。
值得一提的是,TypeScript 中已经实现的 private 公有属性原理就是利用 WeakMap。
公有属性应该是不能被外界拜访到,不能被多个实例共享,JavaScript 中约定俗成地使用下划线来标记公有属性和方法,肯定程度来说是不靠谱的。