這篇文章帶大家了解Vue3中的WeakMap,介紹一下Vue 3 響應式原始碼中為什麼要用 WeakMap 作為「快取區」,希望對大家有幫助。
【相關推薦:《vue.js教學》】
在讀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
物件是一組鍵值對的集合,其中的鍵是 弱引用 的。其鍵必須是 物件,而值可以是任意的。
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
节点作为键名。下面是一个例子。
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
如上,完美解决~~
更多编程相关知识,请访问:编程入门!!
以上是什麼是WeakMap? Vue3響應式原始碼中為什麼要用它作為快取區?的詳細內容。更多資訊請關注PHP中文網其他相關文章!