ahooks 源码解读系列 - 5

862 阅读3分钟

这个系列是将 ahooks 里面的所有 hook 源码都进行解读,通过解读 ahooks 的源码来熟悉自定义 hook 的写法,提高自己写自定义 hook 的能力,希望能够对大家有所帮助。

为了和代码原始注释区分,个人理解部分使用 ///开头,此处和 三斜线指令没有关系,只是为了做区分。

往期回顾

useDynamicList

给列表项关联一个唯一 key,适用于 antd 表单的动态列表,其他场景可能会有问题。

import { useCallback, useRef, useState } from 'react';

export default <T>(initialValue: T[]) => {
  const counterRef = useRef(-1);/// 存储最后一个key,自增
  // key 存储器
  /// 和 list 里面的内容一一对应
  const keyList = useRef<number[]>([]);

  // 内部方法
  const setKey = useCallback((index: number) => {
    counterRef.current += 1;
    keyList.current.splice(index, 0, counterRef.current);
  }, []);

  const [list, setList] = useState(() => {
    (initialValue || []).forEach((_, index) => {
      setKey(index);
    });
    return initialValue || [];
  });
  /// 将列表设置为一个新的列表 newList ,不是将列表重置为最初的状态
  const resetList = useCallback((newList: T[] = []) => {
    keyList.current = []; /// key 存储器清空了,但是 key 没有重新从 0 开始自增
    setList(() => {
      (newList || []).forEach((_, index) => {
        setKey(index);
      });
      return newList || [];
    });
  }, []);
  /// 插入项,并在 keylist 中同样位置插入一个 key
  const insert = useCallback((index: number, obj: T) => {
    setList((l) => {
      const temp = [...l];
      temp.splice(index, 0, obj);
      setKey(index);
      return temp;
    });
  }, []);
  /// 返回指定索引位置数据的 key
  const getKey = useCallback((index: number) => keyList.current[index], []);
  const getIndex = useCallback(
    (key: number) => keyList.current.findIndex((ele) => ele === key),
    [],
  ); /// 传入 key,反向查找对应的数据的索引值
  /// 先给被合并数据设置key,然后合并
  const merge = useCallback((index: number, obj: T[]) => {
    setList((l) => {
      const temp = [...l];
      obj.forEach((_, i) => {
        setKey(index + i);
      });
      temp.splice(index, 0, ...obj);
      return temp;
    });
  }, []);
  /// 替换数据,key不变,如果使用了 key 作为 react 的 key,则替换后该项不会触发重新渲染!!!
  const replace = useCallback((index: number, obj: T) => {
    setList((l) => {
      const temp = [...l];
      temp[index] = obj;
      return temp;
    });
  }, []);

  const remove = useCallback((index: number) => {
    setList((l) => {
      const temp = [...l];
      temp.splice(index, 1);
      /// try catch 用来了防止 current 不存在时程序崩溃
      // remove keys if necessary
      try {
        keyList.current.splice(index, 1);
      } catch (e) {
        console.error(e);
      }
      return temp;
    });
  }, []);

  const move = useCallback((oldIndex: number, newIndex: number) => {
    if (oldIndex === newIndex) {
      return;
    }
    setList((l) => {
      const newList = [...l];
      /// 先把旧索引处数据删除,然后移到新索引处 
      /// 等价于 newList.splice(newIndex, 0, ...newList.splice(oldIndex, 1))
      const temp = newList.filter((_: {}, index: number) => index !== oldIndex);
      temp.splice(newIndex, 0, newList[oldIndex]);

      // move keys if necessary
      try {
        const keyTemp = keyList.current.filter((_: {}, index: number) => index !== oldIndex);
        keyTemp.splice(newIndex, 0, keyList.current[oldIndex]);
        keyList.current = keyTemp;
      } catch (e) {
        console.error(e);
      }

      return temp;
    });
  }, []);

  const push = useCallback((obj: T) => {
    setList((l) => {
      setKey(l.length);
      return l.concat([obj]); /// 使用 concat 是因为 concat 会返回合并后的新数组, push返回的是新数组的长度。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/concat
    });
  }, []);

  const pop = useCallback(() => {
    // remove keys if necessary
    try {
      keyList.current = keyList.current.slice(0, keyList.current.length - 1);
    } catch (e) {
      console.error(e);
    }
    /// 同样,使用 slice 是因为返回的是操作后的新数组,而 pop 返回的是被删除的元素
    setList((l) => l.slice(0, l.length - 1));
  }, []);

  const unshift = useCallback((obj: T) => {
    setList((l) => {
      setKey(0);
      return [obj].concat(l);
    });
  }, []);
  
  /// 这不是一个能在所有场景下使用的方法,是针对 demo 中的 form.getFieldsValue() 获取表单数据然后排序的场景,不了解这点看下面的代码可能会比较懵。
  /// 因为需要 sortForm 方法生效,需要满足下面几点要求:
  /// 1、被传入的 result 源数据必须始终保持最初的索引,因为只有这样他们的 index 才刚好和内部存储的 key 相同
  /// 2、如果使用了 resetList 重置,新增加的 result 索引需要从重置前的最大值开始,而不能从 0 开始。因为重置时没有将 key 重设为 0 开始。例如:本来有三条数据,索引为:0,1,5,resetLis之后新增一项得到的 result 索引应该为 6
  const sortForm = useCallback(
    (result: unknown[]) =>
      result
        .map((item, index) => ({ key: index, item })) // add index into obj
        .sort((a, b) => getIndex(a.key) - getIndex(b.key)) // sort based on the index of table
        .filter((item) => !!item.item) // remove undefined(s)
        .map((item) => item.item), // retrive the data
    [],
  );

  const shift = useCallback(() => {
    // remove keys if necessary
    try {
      keyList.current = keyList.current.slice(1, keyList.current.length);
    } catch (e) {
      console.error(e);
    }
    setList((l) => l.slice(1, l.length));
  }, []);

  return {
    list,
    insert,
    merge,
    replace,
    remove,
    getKey,
    getIndex,
    move,
    push,
    pop,
    unshift,
    shift,
    sortForm,
    resetList,
  };
};

参考资料

以上内容由于本人水平问题难免有误,欢迎大家进行讨论反馈。