Skip to content

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

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

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

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

一、ES6 中的 Set 和 Map

1. Set

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

基本用法

js
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):使用回调函数遍历每个成员。

示例代码

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

基本用法

js
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):使用回调函数遍历每个成员。

示例代码

js
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 非常方便。
    js
    const array = [1, 2, 2, 3]; // 用这个办法去重,是不是简单高效
    const set = new Set(array); // Set { 1, 2, 3 }
  2. 性能

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

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

数组的优势

  1. 有序性

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

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

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

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

Map 的优势

  1. 键的类型多样性

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

    • Map 提供了更方便的迭代方法,如keysvaluesentries
    js
    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. 语法简洁

    • 对象的创建和使用语法更简洁,适合简单的键值对存储。
    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 数据结构的状态。

使用示例

tsx
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的源码大致如下:

ts
// 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 数据结构的状态。

使用示例

tsx
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的源码大致如下:

ts
// 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 自身的属性,后面会有专门的章节进行介绍

ts
const memoizedFn = useMemoizedFn<T>(fn: T): T;

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