这个系列断更了许久,期间发生了不少事情,时机成熟的时候,我会在我的官网和掘金其他板块发个系列慢慢聊,思考提升,我的一些经验和教训希望能够对大家有点小作用。
最近用 vitepress 搭建了下自己的个人 blog 站点,还没做太多的内容更新和同步,有兴趣的同学可以关注下,谢谢。所有文章,分享等等都会在掘金上同步更新。
关于 ahooks 的源码解读,看了下之前的反馈,还有自己的一些认识和思考。后面会调整下,可能是我自己写技术分享在手段和技术还不是不够成熟,总感觉有点啰嗦,我心里呢是怕大家觉得只说这么一点点显的比较粗浅,但是一展开呢,要么方向聊的有点偏差,要么就是词不达意,我自己想呢,还是更专注些,如果有些点需要发散,那么应该聚焦到专门的篇幅去聊,否则会陷入些思想的陷阱,简单的来说我以为你不知道,所以我展开了,其实你知道;还有一种就是我以为你知道,所以我没讲,其实你不知道。
还请大家包涵,毕竟会越写越好的。
一、ES6 中的 Set 和 Map
1. Set
Set 是一种类似于数组的数据结构,但其成员是唯一的、无重复的值。
基本用法
const mySet = new Set([1, 2, 3, 4, 4, 5]);
console.log(mySet); // Set { 1, 2, 3, 4, 5 }
mySet.add(6); // 添加一个新元素
console.log(mySet); // Set { 1, 2, 3, 4, 5, 6 }
mySet.delete(3); // 删除一个元素
console.log(mySet); // Set { 1, 2, 4, 5, 6 }
console.log(mySet.has(4)); // 检查是否包含某个元素,true
mySet.clear(); // 清空所有元素
console.log(mySet); // Set {}
其他方法
size
:返回 Set 对象的成员总数。keys()
:返回 Set 对象的键名集合。values()
:返回 Set 对象的值集合。entries()
:返回 Set 对象的键值对集合。forEach(callbackFn, thisArg)
:使用回调函数遍历每个成员。
示例代码
const mySet = new Set([1, 2, 3]);
// size
console.log(mySet.size); // 3
// keys, values, entries
for (const item of mySet.keys()) {
console.log(item); // 1, 2, 3
}
for (const item of mySet.values()) {
console.log(item); // 1, 2, 3
}
for (const [key, value] of mySet.entries()) {
console.log(key, value); // 1 1, 2 2, 3 3
}
// forEach
mySet.forEach((value) => {
console.log(value); // 1, 2, 3
});
2. Map
Map 是一种键值对的数据结构,其键可以是任意类型的值。
基本用法
const myMap = new Map([
["name", "Alice"],
["age", 30],
]);
console.log(myMap); // Map { 'name' => 'Alice', 'age' => 30 }
myMap.set("gender", "female"); // 设置新的键值对
console.log(myMap); // Map { 'name' => 'Alice', 'age' => 30, 'gender' => 'female' }
console.log(myMap.get("name")); // 获取值,Alice
myMap.delete("age"); // 删除键值对
console.log(myMap); // Map { 'name' => 'Alice', 'gender' => 'female' }
console.log(myMap.has("gender")); // 检查是否包含某个键,true
myMap.clear(); // 清空所有键值对
console.log(myMap); // Map {}
其他方法
size
:返回 Map 对象的成员总数。keys()
:返回 Map 对象的键集合。values()
:返回 Map 对象的值集合。entries()
:返回 Map 对象的键值对集合。forEach(callbackFn, thisArg)
:使用回调函数遍历每个成员。
示例代码
const myMap = new Map([
["name", "Alice"],
["age", 30],
]);
// size
console.log(myMap.size); // 2
// keys, values, entries
for (const key of myMap.keys()) {
console.log(key); // 'name', 'age'
}
for (const value of myMap.values()) {
console.log(value); // 'Alice', 30
}
for (const [key, value] of myMap.entries()) {
console.log(key, value); // 'name' 'Alice', 'age' 30
}
// forEach
myMap.forEach((value, key) => {
console.log(key, value); // 'name' 'Alice', 'age' 30
});
二、Set vs. 数组
Set 的优势
唯一性:
- Set 中的元素是唯一的,自动去重。对于需要存储唯一值的情况,Set 非常方便。
jsconst array = [1, 2, 2, 3]; // 用这个办法去重,是不是简单高效 const set = new Set(array); // Set { 1, 2, 3 }
性能:
- Set 在添加、删除、检查元素是否存在等操作上通常比数组更高效。
jsconst set = new Set([1, 2, 3]); console.log(set.has(2)); // true set.add(4); set.delete(3);
简洁的 API:
- Set 提供了一些简洁的方法,如
add
、delete
、has
和clear
,使操作更为方便。
- Set 提供了一些简洁的方法,如
数组的优势
有序性:
- 数组是有序的,元素按插入顺序排列,适用于需要顺序操作的数据结构。
jsconst array = [1, 2, 3]; console.log(array[0]); // 1
内置方法丰富:
- 数组有大量内置方法(如
map
、filter
、reduce
等),用于各种数据操作和变换。
jsconst array = [1, 2, 3]; const newArray = array.map((x) => x * 2); // [2, 4, 6]
- 数组有大量内置方法(如
支持索引访问:
- 数组支持通过索引快速访问任意位置的元素。
jsconst array = [1, 2, 3]; console.log(array[1]); // 2
三、Map vs. 键值对(对象)
Map 的优势
键的类型多样性:
- Map 的键可以是任意类型,而对象的键只能是字符串或 Symbol。
jsconst map = new Map(); map.set(123, "number key"); map.set(true, "boolean key"); console.log(map.get(123)); // 'number key'
键值对的迭代:
- Map 提供了更方便的迭代方法,如
keys
、values
、entries
。
jsconst map = new Map([ ["name", "Alice"], ["age", 30], ]); for (const [key, value] of map.entries()) { console.log(key, value); // 'name' 'Alice', 'age' 30 }
- Map 提供了更方便的迭代方法,如
性能:
- Map 在频繁增删键值对时通常比对象更高效,尤其在键数量较大时。
键的顺序:
- Map 对象会维护键值对的插入顺序,可以通过迭代器按顺序访问键值对。
对象的优势
语法简洁:
- 对象的创建和使用语法更简洁,适合简单的键值对存储。
js 复制代码 const obj = { name: 'Alice', age: 30 }; console.log(obj.name); // 'Alice'
常用的数据结构:
- 对象是 JavaScript 中最常用的数据结构之一,许多场景下都在使用。
JSON 支持:
- 对象是 JSON 的基础,方便与后端进行数据交换。
适用场景总结
- Set适用于需要存储唯一值的集合,如集合操作、去重等。
- 数组适用于需要有序存储数据的场景,如列表、队列等。
- Map适用于需要键值对存储且键类型多样、操作频繁的场景,如缓存、字典等。
- 对象适用于简单的键值对存储和配置对象等场景。
根据具体需求选择合适的数据结构,可以提高代码的性能和可读性。
四、ahooks 库中的useSet
和useMap
1. useSet
useSet
是一个自定义的 React Hook,用于管理 Set 数据结构的状态。
使用示例
import React from "react";
import { useSet } from "ahooks";
export default () => {
const [set, { add, remove, reset }] = useSet(["Hello"]);
return (
<div>
<button type="button" onClick={() => add(String(Date.now()))}>
Add Timestamp
</button>
<button
type="button"
onClick={() => remove("Hello")}
disabled={!set.has("Hello")}
style={{ margin: "0 8px" }}
>
Remove Hello
</button>
<button type="button" onClick={() => reset()}>
Reset
</button>
<div style={{ marginTop: 16 }}>
<pre>{JSON.stringify(Array.from(set), null, 2)}</pre>
</div>
</div>
);
};
源码分析
useSet
的源码大致如下:
// https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useSet/index.ts
import { useState } from "react";
import useMemoizedFn from "../useMemoizedFn";
function useSet<K>(initialValue?: Iterable<K>) {
const getInitValue = () => new Set(initialValue);
const [set, setSet] = useState<Set<K>>(getInitValue);
const add = (key: K) => {
if (set.has(key)) {
return;
}
setSet((prevSet) => {
const temp = new Set(prevSet);
temp.add(key);
return temp;
});
};
const remove = (key: K) => {
if (!set.has(key)) {
return;
}
setSet((prevSet) => {
const temp = new Set(prevSet);
temp.delete(key);
return temp;
});
};
const reset = () => setSet(getInitValue());
return [
set,
{
add: useMemoizedFn(add),
remove: useMemoizedFn(remove),
reset: useMemoizedFn(reset),
},
] as const;
}
export default useSet;
详细分析
- 初始化状态:
- 使用
useState
初始化一个 Set 对象。getInitValue
函数用于生成初始的 Set 对象。
- 使用
- 添加元素:
add
函数接受一个元素key
,如果 Set 中已经存在该元素,则不做任何操作,否则将其添加到 Set 中。useMemoizedFn
用来优化函数的性能,确保引用的稳定性。
- 删除元素:
remove
函数接受一个元素key
,如果 Set 中不存在该元素,则不做任何操作,否则将其从 Set 中删除。
- 重置 Set:
reset
函数将 Set 重置为初始状态。
2. useMap
useMap
是一个自定义的 React Hook,用于管理 Map 数据结构的状态。
使用示例
import React from "react";
import { useMap } from "ahooks";
export default () => {
const [map, { set, setAll, remove, reset, get }] = useMap<
string | number,
string
>([
["msg", "hello world"],
[123, "number type"],
]);
return (
<div>
<button
type="button"
onClick={() => set(String(Date.now()), new Date().toJSON())}
>
Add
</button>
<button
type="button"
onClick={() => setAll([["text", "this is a new Map"]])}
style={{ margin: "0 8px" }}
>
Set new Map
</button>
<button
type="button"
onClick={() => remove("msg")}
disabled={!get("msg")}
>
Remove 'msg'
</button>
<button type="button" onClick={() => reset()} style={{ margin: "0 8px" }}>
Reset
</button>
<div style={{ marginTop: 16 }}>
<pre>{JSON.stringify(Array.from(map), null, 2)}</pre>
</div>
</div>
);
};
源码分析
useMap
的源码大致如下:
// https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useMap/index.ts
import { useState } from "react";
import useMemoizedFn from "../useMemoizedFn";
function useMap<K, T>(initialValue?: Iterable<readonly [K, T]>) {
const getInitValue = () => new Map(initialValue);
const [map, setMap] = useState<Map<K, T>>(getInitValue);
const set = (key: K, entry: T) => {
setMap((prev) => {
const temp = new Map(prev);
temp.set(key, entry);
return temp;
});
};
const setAll = (newMap: Iterable<readonly [K, T]>) => {
setMap(new Map(newMap));
};
const remove = (key: K) => {
setMap((prev) => {
const temp = new Map(prev);
temp.delete(key);
return temp;
});
};
const reset = () => setMap(getInitValue());
const get = (key: K) => map.get(key);
return [
map,
{
set: useMemoizedFn(set),
setAll: useMemoizedFn(setAll),
remove: useMemoizedFn(remove),
reset: useMemoizedFn(reset),
get: useMemoizedFn(get),
},
] as const;
}
export default useMap;
详细分析
- 初始化状态:
- 使用
useState
初始化一个 Map 对象。getInitValue
函数用于生成初始的 Map 对象。
- 使用
- 设置键值对:
set
函数接受一个键key
和一个值entry
,将该键值对添加到 Map 中。如果 Map 中已经存在该键,则更新其对应的值。
- 设置整个 Map:
setAll
函数接受一个新的 Map,重置当前 Map 为新的 Map。
- 删除键值对:
remove
函数接受一个键key
,如果 Map 中不存在该键,则不做任何操作,否则将其从 Map 中删除。
- 重置 Map:
reset
函数将 Map 重置为初始状态。
- 获取值:
get
函数接受一个键key
,返回该键对应的值。
五、总结
ahooks
中的useSet
和useMap
提供了对 Set 和 Map 这两种 ES6 数据结构的便捷 React Hooks,封装了对 Set 和 Map 的常见操作,如添加、删除、重置等,并使用useState
和useMemoizedFn
来管理和优化这些操作。这些 Hook 的使用使得在 React 组件中管理复杂的数据结构变得更为简单和高效,同时提高了代码的可读性和可维护性。了解这些 Hook 的内部实现有助于我们更好地理解 React 状态管理的原理和技巧。
这里使用的useMemoizedFn
是在 ahooks 内部实现中大量重复利用的一个 hook,当然还包含其他的,比如useLatest
等,这里我们暂且理解为类似useCallback
的简化形式,通过使用该 hook 传入的参数 fn,引用地址永远不会变化,跟useCallback
会有一定的差异的,比如返回的函数并没有继承 fn 自身的属性,后面会有专门的章节进行介绍
const memoizedFn = useMemoizedFn<T>(fn: T): T;
ahooks
对useSet``useMap
封装的初衷就在于简化 React 应用中对这两种数据结构的运用,通过useMemoizedFn
对操作函数进行优化,确保函数引用的稳定性,减少不必要的重新渲染和性能开销,封装后的 API 与 React Hook 的设计风格一致,使用上更加自然和直观,符合 React 的开发习惯。