什么是集合
在学习以下数据结构之前,我们先温习一下集合的概念。
集合(英语:set)简称集,是一个基本的数学模型,指具有某种特定性质的事物的总体。若x是集合A的元素,记作 x ∈ A。集合A也可以用 A = { x, a, b} 表示。 集合具有以下特性:
-
无序性:一个集合中,每个元素的地位都是相同的,元素之间是无序的。集合上可以定义序关系,定义了序关系后,元素之间就可以按照序关系排序。但就集合本身的特性而言,元素之间没有必然的序。(参见序理论)
-
互异性:一个集合中,任何两个元素都认为是不相同的,即每个元素只能出现一次。有时需要对同一元素出现多次的情形进行刻画,可以使用多重集,其中的元素允许出现多次。
-
确定性:给定一个集合,任给一个元素,该元素或者属于或者不属于该集合,二者必居其一,不允许有模棱两可的情况出现。
Set & WeakSet
什么是Set
Set也是 ECMAScript 6 规范中引入的一种数据结构,是一种叫做集合(是由一堆无序的、相关联的,且不重复的内存结构)的数据结构。Set就像一个数组,但是仅包含唯一项。Set对象是值的集合,可以按照插入的顺序迭代它的元素。Set中的元素只会出现一次,即Set中的元素是唯一的。- 存储的值可以是原始值或者是对象引用。
- 与array相比,操作性能做了优化。
使用场景
- Set & Array
// 1.Set 与 Array 互转
let mySet = new Set([1, "some text", {"a": 1, "b": 2}, {"a": 1, "b": 2}]);
let myArr = Array.from(mySet); // [1, "some text", {"a": 1, "b": 2}, {"a": 1, "b": 2}]
// 2.数组去重
const mySet2 = new Set([1, 2, 3, 4, 3]);
mySet2.size; // 4
[...mySet2]; // [1,2,3,4]
- 在vue3中的应用
在vue3响应系统中,有响应式数据和副作用函数,如果在副作用函数中读取了某个响应式数据的属性,在响应式数据属性改变的时候,我们期望副作用函数重新执行,这就是响应系统最核心的功能。那我们如何在响应式数据与副作用函数之间建立联系呢?
- 当读取操作发生时,将副作用函数收集到“桶”中;
- 当设置操作发生时,从“桶”中取出副作用函数并执行。
副作用被收集到“桶”中这个操作使用的就是Set数据结构,副作用函数可能有多个,全部收集到同一个集合中,并利用数据特性,在多次读取时,可以实现去重。
什么是WeakSet
WeakSet对象是一些对象值的集合。且其与Set类似,WeakSet中的每个对象值都只能出现一次。- 在
WeakSet的集合中,所有对象都是唯一的。
它和 Set 对象的主要区别有:
WeakSet只能是对象的集合,而不能像Set那样,可以是任何类型的任意值。WeakSet持弱引用:集合中对象的引用为弱引用。如果没有其他的对WeakSet中对象的引用,那么这些对象会被当成垃圾回收掉。
使用场景
- 作为额外的存储空间,用于判断“是/否”的事实。
比如有一个 messages 数组,在不改变messages数据的情况下,标记消息是否已读。当消息从messages中被删除时,与之相关的标记消息也应该一起消失。
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
let readMessages = new WeakSet();
// 两个消息已读
readMessages.add(messages[0]);
readMessages.add(messages[1]);
// readMessages 包含两个元素
// ……让我们再读一遍第一条消息!
readMessages.add(messages[0]);
// readMessages 仍然有两个不重复的元素
// 回答:message[0] 已读?
alert("Read message 0: " + readMessages.has(messages[0])); // true
messages.shift();
// 现在 readMessages 有一个元素(技术上来讲,内存可能稍后才会被清理)
// 对 传入的 subject 对象 内部存储的所有内容执行回调
function execRecursively(fn, subject, _refs = new WeakSet()) {
// 避免无限递归
if (_refs.has(subject)) {
return;
}
fn(subject);
if (typeof subject === "object") {
_refs.add(subject);
for (const key in subject) {
execRecursively(fn, subject[key], _refs);
}
}
}
const foo = {
foo: "Foo",
bar: {
bar: "Bar",
},
};
foo.bar.baz = foo; // 循环引用!
execRecursively((obj) => console.log(obj), foo);
Map & WeakMap
什么是Map
Map对象是键值对的集合。Map中的一个键只能出现一次;它在Map的集合中是独一无二的。Map对象按键值对迭代——一个for...of循环在每次迭代后会返回一个形式为[key,value]的数组。迭代按插入顺序进行,即键值对按set()方法首次插入到集合中的顺序(也就是说,当调用set()时,map 中没有具有相同值的键)进行迭代。
使用场景
- Map & Object
相同:Object 和 Map 类似的是,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。因此(并且也没有其他内建的替代方式了)过去我们一直都把对象当成 Map 使用。
差异:
| 差异点 | Object | Map |
|---|---|---|
意外的键 | Object有原型链,可能存在拿到不属于本身的键值对 | Map只有插入的键 |
键的类型 | Object键根据不同的迭代方法返回的键是不固定的(for-in 仅包含了以字符串为键的属性;Object.keys 仅包含了对象自身的、可枚举的、以字符串为键的属性;Object.getOwnPropertyNames 包含了所有以字符串为键的属性,即使是不可枚举的;Object.getOwnPropertySymbols 与前者类似,但其包含的是以 Symbol 为键的属性,等等。) | Map的键按插入顺序排列 |
size | Object 的键值对个数无法直接拿到 | Map的可以通过size属性轻易拿到 |
迭代 | Object 没有实现 迭代协议,所以使用 JavaSctipt 的 for...of 表达式并不能直接迭代对象 | Map 是 可迭代的 的,所以可以直接被迭代。 |
性能 | Object在频繁添加和删除键值对的场景下未作出优化 | Map在频繁增删键值对的场景下表现更好 |
序列化和解析 | 原生的由 Object 到 JSON 的序列化支持,使用 JSON.stringify()。原生的由 JSON 到 Object 的解析支持,使用 JSON.parse() | Map没有元素的序列化和解析的支持。(但是你可以使用携带 replacer 参数的 JSON.stringify() 创建一个自己的对 Map 的序列化和解析支持。参见 Stack Overflow 上的提问:How do you JSON.stringify an ES6 Map?) |
const myMap = new Map();
const keyString = 'a string';
const keyObj = {};
const keyFunc = function() {};
// 添加键
myMap.set(keyString, "和键'a string'关联的值");
myMap.set(keyObj, "和键 keyObj 关联的值");
myMap.set(keyFunc, "和键 keyFunc 关联的值");
console.log(myMap.size); // 3
// 读取值
console.log(myMap.get(keyString)); // "和键'a string'关联的值"
console.log(myMap.get(keyObj)); // "和键 keyObj 关联的值"
console.log(myMap.get(keyFunc)); // "和键 keyFunc 关联的值"
console.log(myMap.get('a string')); // "和键'a string'关联的值",因为 keyString === 'a string'
console.log(myMap.get({})); // undefined,因为 keyObj !== {}
console.log(myMap.get(function() {})); // undefined,因为 keyFunc !== function () {}
// 替代Object类数组的结构,让Object回归对象,用于描述更具体的事物
const components = {componentA:'componentA',componentB:'componentB'}
const componentMap = new Map([['componentA','componentA'],['componentB':'componentB']])
- 在vue3中的应用
在之前的响应系统中,收集了对象属性的副作用函数列表,为了和改变的属性对应起来,而不是对象的任何一个属性改变都执行所有的副作用函数,使用了Map数据结构,针对每个对象属性设置唯一的key,每个key对应一个Set结构的副作用函数集合。
这里的target是指响应式数据对象。
什么是WeakMap
WeakMap对象是一组键/值对的集合,其中的键是弱引用的。- 其键必须是对象,而值可以是任意的。
WeakMap的 key 是不可枚举的。
使用场景
- 额外数据的存储,可以存储任意数据
假如我们正在处理一个“属于”另一个代码的一个对象,也可能是第三方库,并想存储一些与之相关的数据,那么这些数据就应该与这个对象共存亡 —— 这时候 WeakMap 正是我们所需要的利器。例如实例的私有属性、与存在用户相关的数据。
我们将这些数据放到 WeakMap 中,并使用该对象作为这些数据的键,那么当该对象被垃圾回收机制回收后,这些数据也会被自动清除。
// john是一个对象
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john,"secret documents");
// 如果 john 消失,secret documents 将会被自动清除
- 在vue3中的应用
在vue3响应系统中,使用WeakMap存储所有需要响应的对象target,这样当target没有任何引用了,说明用户侧不再需要它了,存储的与target相关依赖集合也就跟着一起释放了,不需要额外处理,防止内存溢出。
以下是测试
Map与WeakMap内存使用的代码
// map.js
// 使用 node --expose-gc map.js 执行,--expose-gc允许手动执行垃圾回收
// 计算内存使用大小
function usageSize () {
// 获取 Node.js 进程的内存使用情况(以字节为单位)
const used = process.memoryUsage().heapUsed;
return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}
// 手动执行垃圾回收 garbage collector
global.gc();
console.log(usageSize()); // ≈ 1.85M
let arr = new Array(10 * 1024 * 1024);
const map = new Map();
map.set(arr, 1);
global.gc();
console.log(usageSize()); // ≈ 83.19M
arr = null;
global.gc();
console.log(usageSize()); // ≈ 83.2M
// weakmap.js
// 使用 node --expose-gc weakmap.js 执行,--expose-gc允许手动执行垃圾回收
function usageSize () {
const used = process.memoryUsage().heapUsed;
return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}
global.gc();
console.log(usageSize()); // ≈ 3.19M
let arr = new Array(10 * 1024 * 1024);
const map = new WeakMap();
map.set(arr, 1);
global.gc();
console.log(usageSize()); // ≈ 83.2M
arr = null;
global.gc();
console.log(usageSize()); // ≈ 3.2M
参考:WeakMap and WeakSet(弱映射和弱集合)
Proxy
什么是Proxy
- Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
使用场景
- 数据绑定与观察者模式
实现数据绑定与观察者模式,我们也可以使用 ES5 新增的Object.defineProperty()方法,Vue2 就是使用该技术实现的,而 Vue3 为什么换用 Proxy 需要来实现呢? 所以现在我们先来对比一下这两种实现方法。
优点:操作原数据
缺点:监听的范围有限,不能检测数组和对象的变化,具体表现为
- 对于对象:Vue 无法检测 property 的添加或移除
- 对于数组:无法监听以下改变
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue - 当你修改数组的长度时,例如:
vm.items.length = newLength
// 对象
var o = {
a: 7,
get b() {
return this.a + 1;
},
set c(x) {
this.a = x / 2
}
};
console.log(o.a); // 7
console.log(o.b); // 8
o.c = 50;
console.log(o.a); // 25
优点:可拦截对象的基本语义行为,包括对象的 property 的添加或移除,数组通过索引设置或修改数组长度等。
缺点:需要进行对象代理,操作新对象。
const handler = {
get: function(obj, prop) {
return prop in obj ? obj[prop] : 37;
}
// 还可代理set,has,delete等一系列对象属性读取或设置的捕捉器
...
};
const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37
- 在vue3的应用
在vue3响应系统中,需要监听数据的变化,当读取操作发生时,将副作用函数收集到“桶”中,当设置操作发生时,从“桶”中取出副作用函数并执行。在vue3中使用Proxy代理进行响应式的数据,规避了Object.defineProperty某些情况无法监听的缺陷。
Reflect
什么是Reflect
Reflect 是一个全局对象,其下有许多方法,任何在 Proxy 的拦截器中能够找到的方法,都能够在 Reflect 中找到同名函数。
存在的意义:
- 可以知道执行结果
- 不会因为报错而中断正常的代码逻辑执行。
- Proxy get/set()方法需要的返回值正是Reflect的get/set方法的返回值,并且Reflect可以设置第三个参数receiver,指定调用方,具有不可替代性,可以天然配合使用,比直接对象赋值/获取值要更方便和准确。
使用场景
- 指定调用方
const obj={foo:1}
console.log(Reflect.get(obj, 'foo', { foo: 2 })) // 输出的是 2 而不是 1
- vue3中的应用
// 改造前,用来监听数据获取和设置
const obj = {
foo: 1,
get bar(){
return this.foo
},
set bar(value){
return this.foo = value
}
}
const p = new Proxy(obj, {
get(target, key) {
track(target, key)
// 注意,这里我们没有使用 Reflect.get 完成读取
return target[key]
},
set(target, key, newVal) {
// 这里同样没有使用 Reflect.set 完成设置
target[key] = newVal
trigger(target, key)
}
})
在直接读取和设置foo属性时,是没什么问题的,但是在使用访问器属性getter与setter的情况下,结果可能不符合预期,例如访问bar属性时,getter访问器读取的是foo的值,那么在使用的时候,foo改变的情况下,读取bar属性的副作用函数也应该一起触发。实际情况却是foo改变时,读取bar的副作用函数未触发。
function effect(){
console.log(proxy.bar)
}
p.foo++
这是因为在proxy代理get时,返回的是target[key],target指向obj原始对象,所以等价于读取obj.bar,obj不是proxy代理对象,所以副作用函数没有被收集,也就触发不了。
function effect(){
//等价于
console.log(obj.bar)
}
p.foo++
因此,需要将this指向proxy代理对象,实现依赖收集。
//改造后
const obj = {
foo: 1,
get bar(){
return this.foo
},
set bar(value){
return this.foo = value
}
}
const p = new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
// 使用 Reflect.get 返回读取到的属性值
console.log('target',target)
return Reflect.get(target, key, receiver)
},
set(target, key, newVal, receiver) {
// 使用 Reflect.set 完成设置
Reflect.set(target, key, newVal, receiver)
trigger(target, key)
}
})