• hooks 源码浅析 — Scene


    在这里插入图片描述

    如上图所示,描述了useFusionTable、useAntdTable、usePagination、useRequest四个hook的调用依赖关系。

    usePagination

    基于useRequest封装了常用的分页逻辑,针对入参和出参做了处理。

    • 入参:defaultParams,设置了current和pageSize,会传递给service接口使用。
    • 出参:扩展了pagination,额外返回分页信息以及分页操作函数以便给组件层使用。
    const usePagination = (service, options) => {
        const { defaultPageSize = 10, ...rest } = options;
        
        const result = useRequest(service, {
          defaultParams: [{ current: 1, pageSize: defaultPageSize }],
          refreshDepsAction: () => {
            // eslint-disable-next-line @typescript-eslint/no-use-before-define
            changeCurrent(1);
          },
          ...rest,
        });
      
        const onChange = (c: number, p: number) => {
          let toCurrent = c <= 0 ? 1 : c;
          const toPageSize = p <= 0 ? 1 : p;
          const tempTotalPage = Math.ceil(total / toPageSize);
          if (toCurrent > tempTotalPage) {
            toCurrent = Math.max(1, tempTotalPage);
          }
    
          const [oldPaginationParams = {}, ...restParams] = result.params || [];
    
          result.run(
            {
              ...oldPaginationParams,
              current: toCurrent,
              pageSize: toPageSize,
            },
            ...restParams,
          );
        };
        // changeCurrent、changePageSize逻辑
        return {
          ...result,
          pagination: {
            current,
            pageSize,
            total,
              totalPage,
              onChange: useMemoizedFn(onChange), // 页码或 pageSize 改变的回调
              changeCurrent: useMemoizedFn(changeCurrent),
              changePageSize: useMemoizedFn(changePageSize),
            },
          } as PaginationResult<TData, TParams>;
      };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    useAntdTable

    基于 useRequest 实现,封装了常用的 Ant Design FormAnt Design Table 联动逻辑,并且同时支持 antd v3 和 v4。

    • 出参:扩展了tableProps 和 search 字段,管理表格和表单。提供submit,reset,onTableChange提供给组件层
    • Form与Table联动,需要传递form实例,以便内部处理表单相关行为。例如提交、重置、校验,进一步触发请求逻辑。
    • useUpdateEffect可以忽略首次渲染执行,只在依赖更新的
    const useAntdTable = (service, options) => {
      const {
        form, //接收form实例,实现Form与Table联动
        defaultType = 'simple',
        defaultParams,
        manual = false,
        refreshDeps = [],
        ready = true,
        ...rest
      } = options;
      const result = usePagination<TData, TParams>(service, {
        manual: true,
        ...rest,
      });
      
      const { params = [], run } = result; //params记录缓存的数据
      
      //省略 reset、 submit、 onTableChange 等函数
    
      return {
        ...result,
        tableProps: {
          dataSource: result.data?.list || defaultDataSourceRef.current,
          loading: result.loading,
          onChange: useMemoizedFn(onTableChange),
          pagination: {
            current: result.pagination.current,
            pageSize: result.pagination.pageSize,
            total: result.pagination.total,
          },
        },
        search: {
          submit: useMemoizedFn(submit),
          type,
          changeType: useMemoizedFn(changeType),
          reset: useMemoizedFn(reset),
        },
      } as AntdTableResult<TData, TParams>;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    在这里插入图片描述


    useFusionTable

    上面说到在调用hook依赖中用到了useAntdTable,这里比较巧妙的是用到了适配器模式,针对入参和出参进行了适配转换。

    const useFusionTable = (service, options): FusionTableResult<TData, TParams> => {
      const ret = useAntdTable<TData, TParams>(service, {
        ...options,
        form: options.field ? fieldAdapter(options.field) : undefined,
      });
    
      return resultAdapter(ret);
    };
    
    // 表单实例适配器,操作表单相关函数做映射
    export const fieldAdapter = (field: Field) =>
      ({
        getFieldInstance: (name: string) => field.getNames().includes(name),
        setFieldsValue: field.setValues,
        getFieldsValue: field.getValues,
        resetFields: field.resetToDefault,
        validateFields: (fields, callback) => {
          field.validate(fields, callback);
        },
      } as AntdFormUtils);
    // 结果适配器,磨平Fusion和Antd差异
    export const resultAdapter = (result: any) => {
      const tableProps = {
        dataSource: result.tableProps.dataSource,
        loading: result.tableProps.loading,
        onSort: (dataIndex: string, order: string) => {
          result.tableProps.onChange(
            { current: result.pagination.current, pageSize: result.pagination.pageSize },
            result.params[0]?.filters,
            {
              field: dataIndex,
              order,
            },
          );
        },
        onFilter: (filterParams: Object) => {
          result.tableProps.onChange(
            { current: result.pagination.current, pageSize: result.pagination.pageSize },
            filterParams,
            result.params[0]?.sorter,
          );
        },
      };
    
      const paginationProps = {
        onChange: result.pagination.changeCurrent,
        onPageSizeChange: result.pagination.changePageSize,
        current: result.pagination.current,
        pageSize: result.pagination.pageSize,
        total: result.pagination.total,
      };
    
      return {
        ...result,
        tableProps,
        paginationProps,
      };
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    useInfiniteScroll

    封装了常见的无限滚动逻辑,会自动帮忙整合多次请求的数据结果。

    • 依赖的hook有useRequest、useUpdateEffect、useMemoizedFn、useEventListener。
      const scrollMethod = () => {
        const el = getTargetElement(target);
        if (!el) {
          return;
        }
        
        // 元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。
        const scrollTop = getScrollTop(el); 
        // 元素内容高度的度量
        const scrollHeight = getScrollHeight(el);
        // 元素内部的高度(单位像素),包含内边距,但不包括水平滚动条、边框和外边距。
        const clientHeight = getClientHeight(el);
    
        if (scrollHeight - scrollTop <= clientHeight + threshold) {
          loadMore();
        }
      };
    
      useEventListener(
        'scroll',
        () => {
          if (loading || loadingMore) {
            return;
          }
          scrollMethod();
        },
        { target },
      );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    在这里插入图片描述


    useDynamicList

    管理动态列表状态,并能生成唯一 key 。

    • 内部维护了一个计数器counterRef,和 唯一key的列表。
    • 计数器是不断累加的,移除操作不会改变计数器的当前记录值。只是删除对应的key值。再次添加,会在计数器记录值上继续累加。
    • 增删逻辑转换,以便返回新数组进行赋值。
    • push(item: T): number ===> concat([item]): T[]
    • pop(): T | undefined ===> slice(start?: number, end?: number): T[];
    • unshift(item: T): number ===> concat([item]): T[]
    • shift(): T | undefined ===> slice(start?: number, end?: number): T[];
    const useDynamicList = <T>(initialList: T[] = []) => {
      // 计数器
      const counterRef = useRef(-1);
      // 唯一key列表
      const keyList = useRef<number[]>([]);
      
      const setKey = useCallback((index: number) => {
        counterRef.current += 1;
        keyList.current.splice(index, 0, counterRef.current);
      }, []);
      
      // 基于入参List,记录数量和keyList。
      const [list, setList] = useState(() => {
        initialList.forEach((_, index) => {
          setKey(index);
        });
        return initialList;
      });
      
      const push = useCallback((item: T) => {
        setList((l) => {
          setKey(l.length);
          return l.concat([item]);
        });
      }, []);
      
      const pop = useCallback(() => {
        // remove keys if necessary
        try {
          keyList.current = keyList.current.slice(0, keyList.current.length - 1);
        } catch (e) {
          console.error(e);
        }
    
        setList((l) => l.slice(0, l.length - 1));
      }, []);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    useVirtualList

    提供虚拟化列表能力的 Hook,用于解决展示海量数据渲染时首屏渲染缓慢和滚动卡顿问题。

    • 首先需要清楚几个概览:可视区域、可滚动区域、滚动元素。(如下图所示)
    • 计算当前可见区域起始数据的 startIndex(包含上面区域额外的节点数overscan)
    • 计算当前可见区域结束数据的 endIndex(包含下面区域额外的节点数overscan)
    • 计算内容高度,以及marginTop高度,来抵消上部移除的节点所占据高度。
    • 计算当前可见区域的数据,并渲染到页面中
      在这里插入图片描述
      在这里插入图片描述
    const useVirtualList = <T = any>(list: T[], options: Options<T>) => { const { const useVirtualList = <T = any>(list: T[], options: Options<T>) => {
      const { containerTarget, wrapperTarget, itemHeight, overscan = 5 } = options;
    
      const totalHeight = useMemo(() => {
        if (isNumber(itemHeightRef.current)) {
          return list.length * itemHeightRef.current;
        }
        // @ts-ignore
        // Item 高度不一致时,计算逻辑
        return list.reduce((sum, _, index) => sum + itemHeightRef.current(index, list[index]), 0);
      }, [list]);
      
      // 计算需要渲染Item的索引范围
      const calculateRange = () => {
        const container = getTargetElement(containerTarget); // 可视区域
        const wrapper = getTargetElement(wrapperTarget);     // 可滚动区域
    
        if (container && wrapper) {
          const { scrollTop, clientHeight } = container;
    
          const offset = getOffset(scrollTop); // 顶部偏移的Item数
          const visibleCount = getVisibleCount(clientHeight, offset); //可见Item数
    
          const start = Math.max(0, offset - overscan);
          const end = Math.min(list.length, offset + visibleCount + overscan);
          
          const offsetTop = getDistanceTop(start);// 获取上部高度
          
          // 不渲染真实的节点,用距离来控制。
          // @ts-ignore
          wrapper.style.height = totalHeight - offsetTop + 'px';
          // @ts-ignore
          wrapper.style.marginTop = offsetTop + 'px';
    
          setTargetList(
            list.slice(start, end).map((ele, index) => ({
              data: ele,
              index: index + start,
            })),
          );
        }
      };
    
      useEffect(() => {
        if (!size?.width || !size?.height) {
          return;
        }
        calculateRange();
      }, [size?.width, size?.height, list]);
    
      useEventListener(
        'scroll',
        (e) => {
          if (scrollTriggerByScrollToFunc.current) {
            scrollTriggerByScrollToFunc.current = false;
            return;
          }
          e.preventDefault();
          calculateRange();
        },
        {
          target: containerTarget,
        },
      );
    
      const scrollTo = (index: number) => {
        const container = getTargetElement(containerTarget);
        if (container) {
    
          scrollTriggerByScrollToFunc.current = true; // 标记位,避免二次触发scroll事件
          container.scrollTop = getDistanceTop(index);
          calculateRange();
        }
      };
    
      return [targetList, useMemoizedFn(scrollTo)] as const;
    };
    
    export default useVirtualList;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    useHistoryTravel

    管理状态历史变化记录,方便在历史记录中前进与后退。例如撤销,恢复的场景。

    • 维护了三个变量present、past、future分别记录了当前值,过去及未来的值列表。
    • 更新值时:记录当前值,把原值追加到past中;
    • 回退:从past中进行拆分,同时把原值插入到future中;
    • 前进:从future中进行拆分,同时把原值插入到future中;

    如下表格展示了Input输入后状态的变化,以及前进,回退操作后状态变化。

    input初始值present: undefined, past: [], future: []
    输入1present: “1”, past: [undefined], future: []
    追加输入2present: “12”, past: [undefined, “1”], future: []
    追加输入3present: “123”, past: [undefined, “1”, “12”], future: []
    回退present: “12”, past: [undefined, “1”], future: [“123”]
    回退present: “1”, past: [undefined], future: [“12”, “123”]
    前进present: “12”, past: [undefined, “1”], future: [“123”]
    export default function useHistoryTravel<T>(initialValue?: T) {
      const [history, setHistory] = useState<IData<T | undefined>>({
        present: initialValue, // 记录当前值
        past: [], // 记录过去的值
        future: [], // 记录未来的值
      });
      
      const updateValue = (val: T) => {
        setHistory({
          present: val,
          future: [],
          past: [...past, present], // 更新当前值,把上一次值追加到past数组中
        });
      };
      
       const _backward = (step: number = -1) => {
        if (past.length === 0) {
          return;
        }
    
        const { _before, _current, _after } = split(step, past);
        console.log(_before, _current, _after);
        setHistory({
          past: _before,
          present: _current,
          future: [..._after, present, ...future],
        });
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    useNetwork

    管理网络连接状态。监听online 、 offline、 change事件。初始网络状态通过navigator.onLine获取

    function useNetwork(): NetworkState {
      const [state, setState] = useState(() => {
        // 初始化状态值
        return {
          since: undefined,
          online: navigator?.onLine,
          ...getConnectionProperty(),
        };
      });
    
      useEffect(() => {
        const onOnline = () => {
          setState((prevState) => ({
            ...prevState,
            online: true,
            since: new Date(),
          }));
        };
    
        const onOffline = () => {
          setState((prevState) => ({
            ...prevState,
            online: false,
            since: new Date(),
          }));
        };
    
        const onConnectionChange = () => {
          setState((prevState) => ({
            ...prevState,
            ...getConnectionProperty(),
          }));
        };
    
        window.addEventListener(NetworkEventType.ONLINE, onOnline); // 监听在线状态
        window.addEventListener(NetworkEventType.OFFLINE, onOffline); // 监听离线状态
    
        const connection = getConnection();
        connection?.addEventListener(NetworkEventType.CHANGE, onConnectionChange);
        // 组件卸载,移除事件监听
        return () => {
          window.removeEventListener(NetworkEventType.ONLINE, onOnline);
          window.removeEventListener(NetworkEventType.OFFLINE, onOffline);
          connection?.removeEventListener(NetworkEventType.CHANGE, onConnectionChange);
        };
      }, []);
    
      return state;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    useSelections

    联动 Checkbox 逻辑封装,支持多选,单选,全选逻辑,还提供了是否选择,是否全选,是否半选的状态。

    • 将原始数组转为Set集合,非常方便进行增删查,避免指定唯一ID。思路巧妙
    • Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
    • Set对象是值的集合,你可以按照插入的顺序迭代它的元素。 Set中的元素只会出现一次,即 Set 中的元素是唯一的。
    export default function useSelections<T>(items: T[], defaultSelected: T[] = []) {
      const [selected, setSelected] = useState<T[]>(defaultSelected);
      // 这里将原始数据转为Set数组,采用has方法方便判断唯一性。例如是对象数组,同样支持
      const selectedSet = useMemo(() => new Set(selected), [selected]);
      // 判断是否选中
      const isSelected = (item: T) => selectedSet.has(item);
      // 选择
      const select = (item: T) => {
        selectedSet.add(item);
        return setSelected(Array.from(selectedSet));
      };
      // 取消选择
      const unSelect = (item: T) => {
        selectedSet.delete(item);
        return setSelected(Array.from(selectedSet));
      };
    
      const toggle = (item: T) => {
        if (isSelected(item)) {
          unSelect(item);
        } else {
          select(item);
        }
      };
    
      const selectAll = () => {
        items.forEach((o) => {
          selectedSet.add(o);
        });
        setSelected(Array.from(selectedSet));
      };
    
      const unSelectAll = () => {
        items.forEach((o) => {
          selectedSet.delete(o);
        });
        setSelected(Array.from(selectedSet));
      }
      
      // 省略n行代码
      return {
        selected,
        noneSelected,
        allSelected,
        partiallySelected,
        setSelected,
        isSelected,
        select: useMemoizedFn(select),
        unSelect: useMemoizedFn(unSelect),
        toggle: useMemoizedFn(toggle),
        selectAll: useMemoizedFn(selectAll),
        unSelectAll: useMemoizedFn(unSelectAll),
        toggleAll: useMemoizedFn(toggleAll),
      } as const;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    范例:

    const result: Result= useSelections<T>(items: T[], defaultSelected?: T[]);
    
    useSelections([1, 2, 3, 4, 5, 6, 7, 8]);
    
    useSelections([{ value: 1, label: "苹果" }, { value: 2, label: "梨" }]);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    useCountDown

    用于管理倒计时的 Hook。

    • 动态变更配置项, 适用于验证码或类似场景,时间结束后会触发 onEnd 回调。
    • 借助于setInterval实现,由于间隔不一定精准,以及程序执行需要时间,会存在一定误差。
    • 停止计时并没有提供类似stop函数,而是采用setTargetDate(undefined)更新targetDate
    const calcLeft = (t?: TDate) => {
      if (!t) {
        return 0;
      }
      // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
      const left = dayjs(t).valueOf() - new Date().getTime();
      if (left < 0) {
        return 0;
      }
      return left;
    };
    
    const parseMs = (milliseconds: number): FormattedRes => {
      return {
        days: Math.floor(milliseconds / 86400000),
        hours: Math.floor(milliseconds / 3600000) % 24,
        minutes: Math.floor(milliseconds / 60000) % 60,
        seconds: Math.floor(milliseconds / 1000) % 60,
        milliseconds: Math.floor(milliseconds) % 1000,
      };
    };
    
    const useCountdown = (options?: Options) => {
      // targetDate 截止时间
      const { targetDate, interval = 1000, onEnd } = options || {};
      // 截止时间和当前时间 的差值
      const [timeLeft, setTimeLeft] = useState(() => calcLeft(targetDate));
    
      const onEndRef = useLatest(onEnd);
    
      useEffect(() => {
        if (!targetDate) { //
          // for stop
          setTimeLeft(0);
          return;
        }
    
        // 立即执行一次
        setTimeLeft(calcLeft(targetDate));
    
        const timer = setInterval(() => {
          const targetLeft = calcLeft(targetDate);
          setTimeLeft(targetLeft);
          if (targetLeft === 0) {
            clearInterval(timer);
            onEndRef.current?.();
          }
        }, interval);
    
        return () => clearInterval(timer);
      }, [targetDate, interval]);
    
      const formattedRes = useMemo(() => {
        return parseMs(timeLeft);
      }, [timeLeft]);
    
      return [timeLeft, formattedRes] as const;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    useCounter

    管理计数器的 Hook。

    • 对于临界值的处理。如果指定target不在[min,max]范围内,怎么处理。
    function getTargetValue(val: number, options: Options = {}) {
      const { min, max } = options;
      let target = val;
      if (isNumber(max)) {
        target = Math.min(max, target);
      }
      if (isNumber(min)) {
        target = Math.max(min, target);
      }
      return target;
    }
    
    function useCounter(initialValue: number = 0, options: Options = {}) {
      const { min, max } = options;
    
      const [current, setCurrent] = useState(() => {
        return getTargetValue(initialValue, {
          min,
          max,
        });
      });
    
      const setValue = (value: ValueParam) => {
        setCurrent((c) => {
          const target = isNumber(value) ? value : value(c);
          return getTargetValue(target, {
            max,
            min,
          });
        });
      };
    
      const inc = (delta: number = 1) => {
        setValue((c) => c + delta);
      };
    
      const dec = (delta: number = 1) => {
        setValue((c) => c - delta);
      };
    
      const set = (value: ValueParam) => {
        setValue(value);
      };
    
      const reset = () => {
        setValue(initialValue);
      };
    
      return [
        current,
        {
          inc: useMemoizedFn(inc),
          dec: useMemoizedFn(dec),
          set: useMemoizedFn(set),
          reset: useMemoizedFn(reset),
        },
      ] as const;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    useTextSelection

    实时获取用户当前选取的文本内容及位置。

    • 监听目前元素的down和up事件。鼠标按下,清空原始数据;鼠标抬起重新获取新的选中的值
    • Window.getSelection 返回一个 Selection 对象,表示用户选择的文本范围或光标的当前位置。如果想要将 selection 转换为字符串,可通过连接一个空字符串(“”)或使用 String.toString() 方法
    • Selection.getRangeAt()返回一个包含当前选区内容的区域对象 Range。
    • Range.getBoundingClientRect()返回一个DOMRect对象,{ height, width, top, left, right, bottom }
    • 高阶函数createEffectWithTarget,包装了useEffect,返回一个新函数useEffectWithTarget,扩展了对target的依赖对比。
    export default function depsAreSame(oldDeps: DependencyList, deps: DependencyList): boolean {
      if (oldDeps === deps) return true;
      for (let i = 0; i < oldDeps.length; i++) {
        if (!Object.is(oldDeps[i], deps[i])) return false;
      }
      return true;
    }
    
    const createEffectWithTarget = (useEffectType: typeof useEffect | typeof useLayoutEffect) => {
      /**
       *
       * @param effect
       * @param deps
       * @param target target should compare ref.current vs ref.current, dom vs dom, ()=>dom vs ()=>dom
       */
      const useEffectWithTarget = (
        effect: EffectCallback,
        deps: DependencyList,
        target: BasicTarget<any> | BasicTarget<any>[],
      ) => {
        const hasInitRef = useRef(false);
    
        const lastElementRef = useRef<(Element | null)[]>([]);
        const lastDepsRef = useRef<DependencyList>([]);
    
        const unLoadRef = useRef<any>();
    
        useEffectType(() => {
    
          const targets = isArray(target) ? target : [target];
          const els = targets.map((item) => getTargetElement(item));
    
          // init run
          if (!hasInitRef.current) {
            hasInitRef.current = true;
            lastElementRef.current = els;
            lastDepsRef.current = deps;
    
            unLoadRef.current = effect();
            return;
          }
    
          if (
            els.length !== lastElementRef.current.length ||
            !depsAreSame(els, lastElementRef.current) ||
            !depsAreSame(deps, lastDepsRef.current)
          ) {
            unLoadRef.current?.();
    
            lastElementRef.current = els;
            lastDepsRef.current = deps;
            unLoadRef.current = effect();
          }
        });
    
        useUnmount(() => {
          unLoadRef.current?.();
          // for react-refresh
          hasInitRef.current = false;
        });
      };
    
      return useEffectWithTarget;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    useWebSocket

    用于处理 WebSocket 的 Hook

    • 使用useLatest,返回当前最新值的 Hook,避免闭包问题。
    export default function useWebSocket(socketUrl: string, options: Options = {}): Result {
      const {
        reconnectLimit = 3,
        reconnectInterval = 3 * 1000,
        manual = false,
        onOpen,
        onClose,
        onMessage,
        onError,
        protocols,
      } = options;
      // 返回当前最新值的 Hook,可以避免闭包问题。
      const onOpenRef = useLatest(onOpen);
      const onCloseRef = useLatest(onClose);
      const onMessageRef = useLatest(onMessage);
      const onErrorRef = useLatest(onError);
    
      const reconnectTimesRef = useRef(0);
      const reconnectTimerRef = useRef<ReturnType<typeof setTimeout>>();
      const websocketRef = useRef<WebSocket>();
    
      const unmountedRef = useRef(false);
    
      const [latestMessage, setLatestMessage] = useState<WebSocketEventMap['message']>();
      const [readyState, setReadyState] = useState<ReadyState>(ReadyState.Closed);
    
      // error, close 情况下进行重新连接
      const reconnect = () => {
        if (
          reconnectTimesRef.current < reconnectLimit &&
          websocketRef.current?.readyState !== ReadyState.Open
        ) {
          if (reconnectTimerRef.current) {
            clearTimeout(reconnectTimerRef.current);
          }
    
          reconnectTimerRef.current = setTimeout(() => {
            // eslint-disable-next-line @typescript-eslint/no-use-before-define
            connectWs();
            console.log(2);
    
            reconnectTimesRef.current++;
          }, reconnectInterval);
        }
      };
    
      const connectWs = () => {
        if (reconnectTimerRef.current) {
          clearTimeout(reconnectTimerRef.current);
        }
    
        if (websocketRef.current) {
          websocketRef.current.close();
        }
    
        const ws = new WebSocket(socketUrl, protocols);
        setReadyState(ReadyState.Connecting);
    
        ws.onerror = (event) => {
          if (unmountedRef.current) {
            return;
          }
          console.log(event);
    
          reconnect();
          onErrorRef.current?.(event, ws);
          setReadyState(ws.readyState || ReadyState.Closed);
        };
        ws.onopen = (event) => {
          if (unmountedRef.current) {
            return;
          }
          onOpenRef.current?.(event, ws);
          reconnectTimesRef.current = 0;
          setReadyState(ws.readyState || ReadyState.Open);
        };
        ws.onmessage = (message: WebSocketEventMap['message']) => {
          if (unmountedRef.current) {
            return;
          }
          onMessageRef.current?.(message, ws);
          setLatestMessage(message);
          console.log(message);
    
        };
        ws.onclose = (event) => {
          console.log(event);
          if (unmountedRef.current) {
            return;
          }
          reconnect();
          onCloseRef.current?.(event, ws);
          setReadyState(ws.readyState || ReadyState.Closed);
        };
    
        websocketRef.current = ws;
      };
    
      const sendMessage: WebSocket['send'] = (message) => {
        if (readyState === ReadyState.Open) {
          websocketRef.current?.send(message);
        } else {
          throw new Error('WebSocket disconnected');
        }
      };
    
      const connect = () => {
        reconnectTimesRef.current = 0;
        connectWs();
      };
    
      const disconnect = () => {
        if (reconnectTimerRef.current) {
          clearTimeout(reconnectTimerRef.current);
        }
    
        reconnectTimesRef.current = reconnectLimit;
        websocketRef.current?.close();
      };
    
      useEffect(() => {
        if (!manual) {
          connect();
        }
      }, [socketUrl, manual]);
    
      useUnmount(() => {
        unmountedRef.current = true;
        disconnect();
      });
    
      return {
        latestMessage,
        sendMessage: useMemoizedFn(sendMessage),
        connect: useMemoizedFn(connect),
        disconnect: useMemoizedFn(disconnect),
        readyState,
        webSocketIns: websocketRef.current,
      };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140

    参考资料

  • 相关阅读:
    【毕业设计】33-基于单片机的直流电机的转速检测与控制设计(原理图工程+PCB工程+源代码工程+仿真工程+答辩论文)
    用于NLP领域的排序模型最佳实践
    湖北大学计算机考研资料汇总
    用户中心框架搭建
    css实现三列等宽等间距排列(九宫格)
    福建省发改委福州市营商办莅临育润大健康事业部指导视察工作
    think-cell 数据表无法打开怎么办
    3D激光SLAM:ALOAM---gazebo仿真测试场景搭建
    Typecho博客搭建+cpolar内网穿透实现公网访问内网站点
    相机图像质量研究(35)常见问题总结:图像处理对成像的影响--运动噪声
  • 原文地址:https://blog.csdn.net/Jason847/article/details/126560331