이 글은 Vue3의 WeakMap을 이해하고 Vue 3 반응형 소스 코드에서 WeakMap이 "캐시 영역"으로 사용되는 이유를 소개합니다. 모두에게 도움이 되기를 바랍니다.
【관련 추천: "vue.js Tutorial"】
코드의 Vue 3 반응 원리 부분을 읽다가 반응형 처리를 수행할 때 각 개체 WeakMap에 대해 를 사용하는 것을 보았습니다. code>는 다음 코드를 사용하여 "캐시 영역"을 생성합니다. <code>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
对象是一组键值对的集合,其中的键是 弱引用 的。其键必须是 对象,而值可以是任意的。
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
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);
WeakMap
캐시 영역의 목적은 객체가 반복적으로 프록시되는 것을 방지하는 것입니다. 🎜🎜Vue 3가 프록시 객체를 캐시하기 위해 WeakMap
을 사용하는 이유는 무엇입니까? Map
과 같은 다른 캐시 방법을 사용해 보는 것은 어떨까요? 🎜WeakMap
개체는 키-값 쌍의 모음이며, 여기서 키는 약한 참조입니다. 키는 객체여야 하며 값은 임의적일 수 있습니다. 🎜const testFn = (function () { let data; class Test { constructor(val) { data = val } getData() { return data; } } return Test; })(); let test1 = new testFn(3); let test2 = new testFn(4); console.log(test1.getData()); // 4 console.log(test2.getData()); // 4
Iterable
은 배열(요소 2개 배열) 또는 요소가 키-값 쌍인 기타 반복 가능한 객체입니다. 각 키-값 쌍은 새로운 WeakMap
에 추가됩니다. 🎜WeakMap
에는 get
, set
, has
>, 네 가지 메서드가 있습니다. delete
, 일반적인 사용법을 살펴보겠습니다. 🎜const testFn = (function () { let data = Symbol('data') class Test { constructor(val) { this[data] = val } getData() { return this[data] } } return Test; })(); let test1 = new testFn(3); let test2 = new testFn(4); console.log(test1.getData()); // 3 console.log(test2.getData()); // 4 console.log(test1[Object.getOwnPropertySymbols(test1)[0]]); // 3 console.log(test2[Object.getOwnPropertySymbols(test2)[0]]); // 4
Map
WeakMap
을 사용하는 이유 🎜JavaScript에서 , map
API는 두 개의 배열(하나는 키용, 다른 하나는 값용)을 공유하는 4개의 API 메소드를 통해 구현될 수 있습니다. 이는 map
에 값을 설정할 때 두 배열의 끝에 키와 값을 모두 추가합니다. 이렇게 하면 키와 값 인덱스가 두 배열에서 일치하게 됩니다. 이 맵
에서 값을 가져올 때 모든 키를 반복한 다음 인덱스를 사용하여 값이 저장된 배열에서 해당 값을 검색해야 합니다. 🎜🎜그러나 이러한 구현에는 두 가지 큰 단점이 있습니다. 첫째, 할당 및 검색 작업에는 O(n)
(n
은 키-값 쌍 번호)의 시간 복잡도가 있습니다. ), 두 작업 모두 일치시키기 위해 전체 배열을 순회해야 하기 때문입니다. 🎜🎜또 다른 단점은 배열이 항상 각 키와 값을 참조하기 때문에 메모리 누수를 일으킬 수 있다는 것입니다. 이러한 참조는 다른 참조가 없더라도 가비지 수집 알고리즘이 해당 참조를 재활용하는 것을 방지합니다. 🎜const testFn = (function () { let data = new WeakMap() class Test { constructor(val) { data.set(this, val) } getData() { return data.get(this) } } return Test; })(); let test1 = new testFn(3); let test2 = new testFn(4); console.log(test1.getData()); // 3 console.log(test2.getData()); // 4
Map
의 키로 사용하면 Map
이 존재할 때 개체도 존재하게 됩니다. 메모리를 차지하며 가비지 수집 메커니즘에 의해 회수되지 않습니다. 🎜🎜반면, 네이티브 WeakMap
은 각 키 객체에 대한 약한 참조를 보유합니다. 이는 다른 참조가 없을 때 가비지 수집이 올바르게 진행될 수 있음을 의미합니다. 🎜🎜WeakMap
의 key
가 열거 가능하지 않은 것은 바로 이러한 약한 참조 때문입니다(모든 key
를 제공할 수 있는 메서드는 없습니다). key
가 열거 가능한 경우 해당 목록은 가비지 수집 메커니즘의 영향을 받아 정의되지 않은 결과가 발생합니다. 따라서 이러한 유형의 객체에 대한 키
값 목록을 원할 경우 Map
을 사용해야 합니다. 🎜🎜요약하면 다음과 같은 결론을 내릴 수 있습니다. WeakMap의 키가 가리키는 객체는 가비지 수집 메커니즘에서 계산되지 않습니다. 🎜🎜따라서 객체에 데이터를 추가하고 가비지 수집 메커니즘을 방해하고 싶지 않다면 WeakMap
을 사용할 수 있습니다. 🎜🎜이 내용을 보신 분들은 모두 아시겠지만, Vue 3가 WeakMap
을 버퍼로 사용하는 이유는 더 이상 사용되지 않는 데이터를 올바르게 가비지 수집하기 위한 것입니다. 🎜컴퓨터 프로그래밍에서는 약한 참조 및 강한 참조 Strong>relative는 참조하는 개체가 가비지 수집기에 의해 수집되지 않는다는 것을 보장하지 않는 참조를 나타냅니다. 객체가 약한 참조에 의해서만 참조되는 경우 객체는 액세스할 수 없는(또는 약한 액세스) 것으로 간주되므로 언제든지 재활용될 수 있습니다.
V8
엔진이 가비지 수집을 수행하는 방식을 이해해야 합니다. 🎜🎜JSer
의 경우 메모리 관리는 자동으로 눈에 보이지 않습니다. V8
엔진 덕분에 은밀하게 뒤에서 사용되지 않는 메모리를 찾아 정리하도록 도와줍니다. 🎜🎜그렇다면 더 이상 필요하지 않은 것이 있으면 어떻게 되며, V8
엔진은 이를 어떻게 찾아서 정리할까요? 🎜现在各大浏览器通常用采用的垃圾回收有两种方法,一种是「引用计数」,另外一种就是「标记清除」。下面我们来看一下:
标记清除被称为 mark-and-sweep
,它是基于 可达性 来判断对象是否存活的,它会定期执行以下「垃圾回收」步骤:
垃圾收集器找到所有的根,并标记(记住)它们。
然后它遍历并标记来自它们的所有引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
……如此操作,直到所有可达的(从根部)引用都被访问到。
没有被标记的对象都会被删除。
我们还可以将这个过程想象成从根溢出一个巨大的油漆桶,它流经所有引用并标记所有可到达的对象,然后移除未标记的。
引用计数方式最基本的形态就是让每个被管理的对象与一个引用计数器关联在一起,该计数器记录着该对象当前被引用的次数,每当创建一个新的引用指向该对象时其计数器就加 1,每当指向该对象的引用失效时计数器就减 1。当该计数器的值降到 0 就认为对象死亡。
引用计数与基于「可达性」的标记清除的内存管理方式最大的区别就是,前者只需要 局部的信息,而后者需要 全局的信息。
在引用计数中每个计数器只记录了其对应对象的局部信息 —— 被引用的次数,而没有(也不需要)一份全局的对象图的生死信息。
由于只维护局部信息,所以不需要扫描全局对象图就可以识别并释放死对象。但也因为缺乏全局对象图信息,所以 无法处理循环引用 的状况。
所以,更高级的引用计数实现会引入 弱引用 的概念来打破某些已知的循环引用。
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 中约定俗成地使用下划线来标记私有属性和方法,一定程度来说是不靠谱的。
下面我们用三种方法来实现:
const testFn = (function () { let data; class Test { constructor(val) { data = val } getData() { return data; } } return Test; })(); let test1 = new testFn(3); let test2 = new testFn(4); console.log(test1.getData()); // 4 console.log(test2.getData()); // 4
可以看到最后都输出 4
,多实例共享私有属性了,所以版本一不符合。
const testFn = (function () { let data = Symbol('data') class Test { constructor(val) { this[data] = val } getData() { return this[data] } } return Test; })(); let test1 = new testFn(3); let test2 = new testFn(4); console.log(test1.getData()); // 3 console.log(test2.getData()); // 4 console.log(test1[Object.getOwnPropertySymbols(test1)[0]]); // 3 console.log(test2[Object.getOwnPropertySymbols(test2)[0]]); // 4
使用 Symbol
虽然实现了而且正确输出了 3
、4
,但是我们发现可以在外界不通过 getData
方法直接拿到私有属性,所以这种方法也不满足我们的要求。
WeakMap
const testFn = (function () { let data = new WeakMap() class Test { constructor(val) { data.set(this, val) } getData() { return data.get(this) } } return Test; })(); let test1 = new testFn(3); let test2 = new testFn(4); console.log(test1.getData()); // 3 console.log(test2.getData()); // 4
如上,完美解决~~
更多编程相关知识,请访问:编程入门!!
위 내용은 위크맵이란 무엇인가요? Vue3 반응형 소스 코드에서 캐시 영역으로 사용되는 이유는 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!