关于vue.js:Vue-3-响应式源码中为什么使用-WeakMap-作为缓存区

56次阅读

共计 5030 个字符,预计需要花费 13 分钟才能阅读完成。


在读 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 有四个办法:别离是 getsethasdelete,上面咱们看一下其大抵的用法:

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 持有的是每个键对象的 弱援用,这意味着在没有其余援用存在时垃圾回收能正确进行。

正是因为这样的弱援用,WeakMapkey 是不可枚举的 (没有办法能给出所有的 key)。如果 key 是可枚举的话,其列表将会受垃圾回收机制的影响,从而失去不确定的后果。因而,如果你想要这种类型对象的 key 值的列表,你应该应用 Map

综上,咱们能够得出以下论断:WeakMap 的键所指向的对象,不计入垃圾回收机制

所以,如果你要往对象上增加数据,又不想烦扰垃圾回收机制,就能够应用 WeakMap

看到这里大家就应该晓得了,Vue 3 之所以应用 WeakMap 来作为缓冲区就是为了能将 不再应用的数据进行正确的垃圾回收

什么是弱援用

对于「弱援用」,维基百科给出了答案:

在计算机程序设计中,弱援用 强援用 绝对,是指不能确保其援用的对象不会被垃圾回收器回收的援用。一个对象若只被弱援用所援用,则被认为是不可拜访(或弱可拜访)的,并因而 可能在任何时刻被回收

为什么会呈现弱援用

那么,为什么会呈现弱援用呢?弱援用除了能解决上述问题之外还能解决什么问题呢?要想答复这些问题,咱们首先须要理解一下 V8 引擎是如何进行垃圾回收的。

对于 JSer 来说,内存的治理是主动的、有形的,这所有都归功于 V8 引擎在背地默默地帮咱们找到不须要应用的内存并进行清理。

那么,当咱们不再须要某个货色时会产生什么,V8 引擎又是如何发现并清理它的呢?

当初各大浏览器通常用采纳的垃圾回收有两种办法,一种是「援用计数」,另外一种就是「标记革除」。上面咱们来看一下:

标记革除

标记革除被称为 mark-and-sweep,它是基于 可达性 来判断对象是否存活的,它会定期执行以下「垃圾回收」步骤:

  1. 垃圾收集器找到所有的根,并标记(记住)它们。
  2. 而后它遍历并标记来自它们的所有援用。所有被遍历到的对象都会被记住,免得未来再次遍历到同一个对象。
  3. ……如此操作,直到所有可达的(从根部)援用都被拜访到。
  4. 没有被标记的对象都会被删除。

咱们还能够将这个过程设想成从根溢出一个微小的油漆桶,它流经所有援用并标记所有可达到的对象,而后移除未标记的。

援用计数

援用计数形式最根本的状态就是让每个被治理的对象与一个援用计数器关联在一起,该计数器记录着该对象以后被援用的次数,每当创立一个新的援用指向该对象时其计数器就加 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 中约定俗成地应用下划线来标记公有属性和办法,肯定水平来说是不靠谱的。

上面咱们用三种办法来实现:

  • 版本一:闭包
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,多实例共享公有属性了,所以版本一不合乎。

  • 版本二:Symbol
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 尽管实现了而且正确输入了 34,然而咱们发现能够在外界不通过 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

如上,完满解决~~

参考

更多精彩请关注咱们的公众号“百瓶技术”,有不定期福利呦!

正文完
 0