【ahooks源码解读05】useSet和useMap

这个系列断更了许久,期间发生了不少事情,时机成熟的时候,我会在我的官网[1]和掘金其他板块发个系列慢慢聊,思考提升,我的一些经验和教训希望能够对大家有点小作用。

最近用 vitepress 搭建了下自己的个人 blog 站点,还没做太多的内容更新和同步,有兴趣的同学可以关注下,谢谢。所有文章,分享等等都会在掘金上同步更新。

关于ahooks[2]的源码解读,看了下之前的反馈,还有自己的一些认识和思考。后面会调整下,可能是我自己写技术分享在手段和技术还不是不够成熟,总感觉有点啰嗦,我心里呢是怕大家觉得只说这么一点点显的比较粗浅,但是一展开呢,要么方向聊的有点偏差,要么就是词不达意,我自己想呢,还是更专注些,如果有些点需要发散,那么应该聚焦到专门的篇幅去聊,否则会陷入些思想的陷阱,简单的来说我以为你不知道,所以我展开了,其实你知道;还有一种就是我以为你知道,所以我没讲,其实你不知道

还请大家包涵,毕竟会越写越好的。

一、ES6 中的 Set 和 Map

1. Set

Set 是一种类似于数组的数据结构,但其成员是唯一的、无重复的值。

基本用法

scss 复制代码
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):使用回调函数遍历每个成员。

示例代码

javascript 复制代码
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 是一种键值对的数据结构,其键可以是任意类型的值。

基本用法

ruby 复制代码
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):使用回调函数遍历每个成员。

示例代码

javascript 复制代码
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 的优势

  1. 唯一性

    • Set 中的元素是唯一的,自动去重。对于需要存储唯一值的情况,Set 非常方便。
    c 复制代码
    const array = [1, 2, 2, 3]; // 用这个办法去重,是不是简单高效
    const set = new Set(array); // Set { 1, 2, 3 }
  2. 性能

    • Set 在添加、删除、检查元素是否存在等操作上通常比数组更高效。
    vbscript 复制代码
    const set = new Set([1, 2, 3]);
    console.log(set.has(2)); // true
    set.add(4);
    set.delete(3);
  3. 简洁的 API

    • Set 提供了一些简洁的方法,如adddeletehasclear,使操作更为方便。

数组的优势

  1. 有序性

    • 数组是有序的,元素按插入顺序排列,适用于需要顺序操作的数据结构。
    c 复制代码
    const array = [1, 2, 3];
    console.log(array[0]); // 1
  2. 内置方法丰富

    • 数组有大量内置方法(如mapfilterreduce等),用于各种数据操作和变换。
    c 复制代码
    const array = [1, 2, 3];
    const newArray = array.map(x => x * 2); // [2, 4, 6]
  3. 支持索引访问

    • 数组支持通过索引快速访问任意位置的元素。
    c 复制代码
    const array = [1, 2, 3];
    console.log(array[1]); // 2

三、Map vs. 键值对(对象)

Map 的优势

  1. 键的类型多样性

    • Map 的键可以是任意类型,而对象的键只能是字符串或 Symbol。
    c 复制代码
    const map = new Map();
    map.set(123, 'number key');
    map.set(true, 'boolean key');
    console.log(map.get(123)); // 'number key'
  2. 键值对的迭代

    • Map 提供了更方便的迭代方法,如keysvaluesentries
    arduino 复制代码
    const map = new Map([
      ['name', 'Alice'],
      ['age', 30]
    ]);
    for (const [key, value] of map.entries()) {
      console.log(key, value); // 'name' 'Alice', 'age' 30
    }
  3. 性能

    • Map 在频繁增删键值对时通常比对象更高效,尤其在键数量较大时。
  4. 键的顺序

    • Map 对象会维护键值对的插入顺序,可以通过迭代器按顺序访问键值对。

对象的优势

  1. 语法简洁

    • 对象的创建和使用语法更简洁,适合简单的键值对存储。
    ini 复制代码
    js
    复制代码
    const obj = { name: 'Alice', age: 30 };
    console.log(obj.name); // 'Alice'
  2. 常用的数据结构

    • 对象是 JavaScript 中最常用的数据结构之一,许多场景下都在使用。
  3. JSON 支持

    • 对象是 JSON 的基础,方便与后端进行数据交换。

适用场景总结

  • Set适用于需要存储唯一值的集合,如集合操作、去重等。
  • 数组适用于需要有序存储数据的场景,如列表、队列等。
  • Map适用于需要键值对存储且键类型多样、操作频繁的场景,如缓存、字典等。
  • 对象适用于简单的键值对存储和配置对象等场景。

根据具体需求选择合适的数据结构,可以提高代码的性能和可读性。

四、ahooks 库中的useSetuseMap

1. useSet

useSet是一个自定义的 React Hook,用于管理 Set 数据结构的状态。

使用示例

javascript 复制代码
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的源码大致如下:

javascript 复制代码
// 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;

详细分析

  1. 初始化状态

    • 使用useState初始化一个 Set 对象。getInitValue函数用于生成初始的 Set 对象。
  2. 添加元素

    • add函数接受一个元素key,如果 Set 中已经存在该元素,则不做任何操作,否则将其添加到 Set 中。useMemoizedFn用来优化函数的性能,确保引用的稳定性。
  3. 删除元素

    • remove函数接受一个元素key,如果 Set 中不存在该元素,则不做任何操作,否则将其从 Set 中删除。
  4. 重置 Set

    • reset函数将 Set 重置为初始状态。

2. useMap

useMap是一个自定义的 React Hook,用于管理 Map 数据结构的状态。

使用示例

typescript 复制代码
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的源码大致如下:

typescript 复制代码
// 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;

详细分析

  1. 初始化状态

    • 使用useState初始化一个 Map 对象。getInitValue函数用于生成初始的 Map 对象。
  2. 设置键值对

    • set函数接受一个键key和一个值entry,将该键值对添加到 Map 中。如果 Map 中已经存在该键,则更新其对应的值。
  3. 设置整个 Map

    • setAll函数接受一个新的 Map,重置当前 Map 为新的 Map。
  4. 删除键值对

    • remove函数接受一个键key,如果 Map 中不存在该键,则不做任何操作,否则将其从 Map 中删除。
  5. 重置 Map

    • reset函数将 Map 重置为初始状态。
  6. 获取值

    • get函数接受一个键key,返回该键对应的值。

五、总结

ahooks中的useSetuseMap提供了对 Set 和 Map 这两种 ES6 数据结构的便捷 React Hooks,封装了对 Set 和 Map 的常见操作,如添加、删除、重置等,并使用useStateuseMemoizedFn来管理和优化这些操作。这些 Hook 的使用使得在 React 组件中管理复杂的数据结构变得更为简单和高效,同时提高了代码的可读性和可维护性。了解这些 Hook 的内部实现有助于我们更好地理解 React 状态管理的原理和技巧。

这里使用的useMemoizedFn是在 ahooks 内部实现中大量重复利用的一个 hook,当然还包含其他的,比如useLatest等,这里我们暂且理解为类似useCallback的简化形式,通过使用该 hook 传入的参数 fn,引用地址永远不会变化,跟useCallback会有一定的差异的,比如返回的函数并没有继承 fn 自身的属性,后面会有专门的章节进行介绍

ini 复制代码
const memoizedFn = useMemoizedFn<T>(fn: T): T;

ahooksuseSet``useMap封装的初衷就在于简化 React 应用中对这两种数据结构的运用,通过useMemoizedFn对操作函数进行优化,确保函数引用的稳定性,减少不必要的重新渲染和性能开销,封装后的 API 与 React Hook 的设计风格一致,使用上更加自然和直观,符合 React 的开发习惯。

参考资料

1\] 我的官网: *[miaodaddy.xyz/](https://link.juejin.cn?target=https%3A%2F%2Fmiaodaddy.xyz%2F "https://miaodaddy.xyz/")* \[2\] ahooks: *[ahooks.js.org/](https://link.juejin.cn?target=https%3A%2F%2Fahooks.js.org%2F "https://ahooks.js.org/")*

相关推荐
Z_Wonderful2 小时前
在 Next.js 中,使用 [id] 或 public 作为文件夹或文件名是两种完全不同的概念,分别对应 动态路由 和 静态资源托管
javascript·网络·chrome
咬人喵喵2 小时前
E2.COOL 平台深度解析:从特效分类到实战操作指南
前端·编辑器·svg
RisunJan3 小时前
Linux命令-named-checkzone
linux·前端
小陈工3 小时前
Python Web开发入门(十):数据库迁移与版本管理——让数据库变更可控可回滚
前端·数据库·人工智能·python·sql·云原生·架构
吹晚风吧3 小时前
解决vite打包,base配置前缀,nginx的dist包找不到资源
服务器·前端·nginx
weixin199701080163 小时前
《施耐德商品详情页前端性能优化实战》
前端·性能优化
不想上班只想要钱4 小时前
模板里 item.xxx 报错 ,报 item的类型为未知
前端·vue
Irene19914 小时前
推荐 React 开发需要在 VS Code 中安装的插件
react.js
妖萌妹儿4 小时前
postman怎么做参数化批量测试,测试不同输入组合
开发语言·javascript·postman
阿琳a_4 小时前
在github上部署个人的vitepress文档网站
前端·vue.js·github·网站搭建·cesium