ahooks 源码解读系列 - 11

856 阅读4分钟

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

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

往期回顾

今天是 State 部分的最后一篇,至此 State 部分的 17 个 hook 都解读完毕,谢谢大家拨冗前来阅读🙏~

useCountDown

“全场清仓,最后三天,最后三天。。。”

import { useEffect, useMemo, useState } from 'react';
import dayjs from 'dayjs';
import usePersistFn from '../usePersistFn';

/// ...

/// 计算剩余时间的核心方法
const calcLeft = (t?: TDate) => {
  if (!t) {
    return 0;
  }
  
  /// 此处其实可以使用一个中间变量先计算好 dayjs(t).valueOf() 的值,这样就不用每次都重新计算了
  /// 而且在计算的时候可以兼容一下下面那种 issue 的情况,因为这种问题没遇到的人可能就不知道有这个坑
  /// 然后就为了拿一个时间对应的时间戳就引入了 dayjs 一整个库???new Date 表示有被气到
  // 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) => {
  const { targetDate, interval = 1000, onEnd } = options || {};

  const [target, setTargetDate] = useState<TDate>(targetDate);
  const [timeLeft, setTimeLeft] = useState(() => calcLeft(target));

  const onEndPersistFn = usePersistFn(() => {
    if (onEnd) {
      onEnd();
    }
  });

  /// 根据设置的间隔定时计算剩余时间
  useEffect(() => {
    if (!target) {
      // for stop
      setTimeLeft(0);
      return;
    }

    // 立即执行一次
    setTimeLeft(calcLeft(target));

    const timer = setInterval(() => {
      const targetLeft = calcLeft(target);
      setTimeLeft(targetLeft);
      if (targetLeft === 0) {
        clearInterval(timer);
          onEndPersistFn();
      }
    }, interval);

    return () => clearInterval(timer);
  }, [target, interval]);

  const formattedRes = useMemo(() => {
    return parseMs(timeLeft);
  }, [timeLeft]);

  return [timeLeft, setTargetDate, formattedRes] as const;
};

export default useCountdown;

useHistoryTravel

“曾经有一份真挚的爱情摆在我面前。。。”

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

/// ...
/// 根据指定的步数得到最终在数组中的索引
const dumpIndex = <T>(step: number, arr: T[]) => {
  let index =
    step > 0
      ? step - 1 // move forward
      : arr.length + step; // move backward
  if (index >= arr.length - 1) {
    index = arr.length - 1;
  }
  if (index < 0) {
    index = 0;
  }
  return index;
};
/// 核心方法,根据步数将指定数组分割为 过去、现在、未来 三部分
const split = <T>(step: number, targetArr: T[]) => {
  const index = dumpIndex(step, targetArr);
  return {
    _current: targetArr[index],
    _before: targetArr.slice(0, index),
    _after: targetArr.slice(index + 1)
  };
};

export default function useHistoryTravel<T>(initialValue?: T) {
  const [history, setHistory] = useState<IData<T | undefined>>({
    present: initialValue,
    past: [],
    future: []
  });

  const { present, past, future } = history;

  const initialValueRef = useRef(initialValue);
  /// 往事随风去,明天又是新的一天
  const reset = useCallback(
    (...params: any[]) => {
      const _initial = params.length > 0 ? params[0] : initialValueRef.current;
      initialValueRef.current = _initial;

      setHistory({
        present: _initial,
        future: [],
        past: []
      });
    },
    [history, setHistory]
  );
  /// 本来没有路,走了就有了
  const updateValue = useCallback(
    (val: T) => {
      setHistory({
        present: val,
        future: [],
        past: [...past, present]
      });
    },
    [history, setHistory]
  );
  /// 往事不可忆,来者犹可追
  const _forward = useCallback(
    (step: number = 1) => {
      if (future.length === 0) {
        return;
      }
      const { _before, _current, _after } = split(step, future);
      setHistory({
        past: [...past, present, ..._before],
        present: _current,
        future: _after
      });
    },
    [history, setHistory]
  );
  /// 我想回去找一个脚底有七个痣的人
  const _backward = useCallback(
    (step: number = -1) => {
      if (past.length === 0) {
        return;
      }

      const { _before, _current, _after } = split(step, past);
      setHistory({
        past: _before,
        present: _current,
        future: [..._after, present, ...future]
      });
    },
    [history, setHistory]
  );
  /// 时间领主就是我
  const go = useCallback(
    (step: number) => {
      const stepNum = typeof step === 'number' ? step : Number(step);
      if (stepNum === 0) {
        return;
      }
      if (stepNum > 0) {
        return _forward(stepNum);
      }
      _backward(stepNum);
    },
    [_backward, _forward]
  );

  return {
    value: present,
    setValue: updateValue,
    backLength: past.length,
    forwardLength: future.length,
    go,
    back: useCallback(() => {
      go(-1);
    }, [go]),
    forward: useCallback(() => {
      go(1);
    }, [go]),
    reset
  };
}

useNetwork

“你家网速咋样”

import { useEffect, useState } from 'react';

/// ...
/// 依赖于 window.navigator
function getConnection() {
  const nav = navigator as any;
  if (typeof nav !== 'object') return null;
  return nav.connection || nav.mozConnection || nav.webkitConnection;
}

function getConnectionProperty(): NetworkState {
  const c = getConnection();
  if (!c) return {};
  return {
    rtt: c.rtt,
    type: c.type,
    saveData: c.saveData,
    downlink: c.downlink,
    downlinkMax: c.downlinkMax,
    effectiveType: c.effectiveType,
  };
}

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('online', onOnline);
    window.addEventListener('offline', onOffline);

    const connection = getConnection();
    connection?.addEventListener('change', onConnectionChange);

    return () => {
      window.removeEventListener('online', onOnline);
      window.removeEventListener('offline', onOffline);
      connection?.removeEventListener('change', onConnectionChange);
    };
  }, []);

  return state;
}

export default useNetwork;

useWebSocket

“喂喂喂,你在哪里呀”

import useUnmount from '../useUnmount';
import usePersistFn from '../usePersistFn';

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

export enum ReadyState {
  Connecting = 0,
  Open = 1,
  Closing = 2,
  Closed = 3,
}

/// ...

export default function useWebSocket(socketUrl: string, options: Options = {}): Result {
  const {
    reconnectLimit = 3,
    reconnectInterval = 3 * 1000,
    manual = false,
    onOpen,
    onClose,
    onMessage,
    onError,
  } = options;

  const reconnectTimesRef = useRef(0);
  const reconnectTimerRef = useRef<NodeJS.Timeout>();
  const websocketRef = useRef<WebSocket>();

  const [latestMessage, setLatestMessage] = useState<WebSocketEventMap['message']>();
  const [readyState, setReadyState] = useState<ReadyState>(ReadyState.Closed);

  /**
   * 重连
   */
  const reconnect = usePersistFn(() => {
    if (
      reconnectTimesRef.current < reconnectLimit &&
      websocketRef.current?.readyState !== ReadyState.Open
    ) {
      reconnectTimerRef.current && clearTimeout(reconnectTimerRef.current);
      /// 一直重试知道连上或者次数达到最大值
      reconnectTimerRef.current = setTimeout(() => {
        connectWs();
        reconnectTimesRef.current++;
      }, reconnectInterval);
    }
  });
  /// 连接 ws 然后注册一堆事件用来同步状态
  const connectWs = usePersistFn(() => {
    reconnectTimerRef.current && clearTimeout(reconnectTimerRef.current);

    if (websocketRef.current) {
      websocketRef.current.close();
    }

    try {
      websocketRef.current = new WebSocket(socketUrl);
      websocketRef.current.onerror = (event) => {
        reconnect();
        onError && onError(event);
        setReadyState(websocketRef.current?.readyState || ReadyState.Closed);
      };
      websocketRef.current.onopen = (event) => {
        onOpen && onOpen(event);
        reconnectTimesRef.current = 0;
        setReadyState(websocketRef.current?.readyState || ReadyState.Closed);
      };
      websocketRef.current.onmessage = (message: WebSocketEventMap['message']) => {
        onMessage && onMessage(message);
        setLatestMessage(message);
      };
      websocketRef.current.onclose = (event) => {
        reconnect();
        onClose && onClose(event);
        setReadyState(websocketRef.current?.readyState || ReadyState.Closed);
      };
    } catch (error) {
      throw error;
    }
  });

  /**
   * 发送消息
   * @param message
   */
  const sendMessage: WebSocket['send'] = usePersistFn((message) => {
    if (readyState === ReadyState.Open) {
      websocketRef.current?.send(message);
    } else {
      throw new Error('WebSocket disconnected');
    }
  });

  /**
   * 手动 connect
   */
  const connect = usePersistFn(() => {
    reconnectTimesRef.current = 0;
    connectWs();
  });

  /**
   * disconnect websocket
   */
  const disconnect = usePersistFn(() => {
    reconnectTimerRef.current && clearTimeout(reconnectTimerRef.current);

    reconnectTimesRef.current = reconnectLimit;
    websocketRef.current?.close();
  });

  useEffect(() => {
    // 初始连接
    if (!manual) {
      connect();
    }
  }, [socketUrl, manual]);

  useUnmount(() => {
    disconnect();
  });

  return {
    latestMessage,
    sendMessage,
    connect,
    disconnect,
    readyState,
    webSocketIns: websocketRef.current,
  };
}

useWhyDidYouUpdate

“粤康码出示一下”

记录每一次 props 属性的变更

import { useEffect, useRef } from 'react';

export type IProps = {
  [key: string]: any;
};

export default function useWhyDidYouUpdate(componentName: string, props: IProps) {
  const prevProps = useRef<IProps>({});

  useEffect(() => {
    if (prevProps.current) {
      const allKeys = Object.keys({ ...prevProps.current, ...props });
      const changedProps: IProps = {};

      allKeys.forEach((key) => {
        /// 使用 !== 比较
        if (prevProps.current![key] !== props[key]) {
          changedProps[key] = {
            from: prevProps.current![key],
            to: props[key],
          };
        }
      });

      if (Object.keys(changedProps).length) {
        console.log('[why-did-you-update]', componentName, changedProps);
      }
    }

    prevProps.current = props;
  });
}

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