likes
comments
collection
share

ahooks 源码浅析 — Scene

作者站长头像
站长
· 阅读数 35

ahooks 源码浅析 — 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>;
  };

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>;
};

ahooks 源码浅析 — Scene


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,
  };
};

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 },
  );

ahooks 源码浅析 — Scene


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));
  }, []);

}

useVirtualList

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

  • 首先需要清楚几个概览:可视区域、可滚动区域、滚动元素。(如下图所示)
  • 计算当前可见区域起始数据的 startIndex(包含上面区域额外的节点数overscan)
  • 计算当前可见区域结束数据的 endIndex(包含下面区域额外的节点数overscan)
  • 计算内容高度,以及marginTop高度,来抵消上部移除的节点所占据高度。
  • 计算当前可见区域的数据,并渲染到页面中

ahooks 源码浅析 — Scene

ahooks 源码浅析 — Scene

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;


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],
    });
  };
}

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;
}

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;
}

范例:

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: "梨" }]);

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;
};

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;
}

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;
};

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,
  };
}

参考资料