这个系列是将 ahooks 里面的所有 hook 源码都进行解读,通过解读 ahooks 的源码来熟悉自定义 hook 的写法,提高自己写自定义 hook 的能力,希望能够对大家有所帮助。
为了和代码原始注释区分,个人理解部分使用 ///
开头,此处和 三斜线指令没有关系,只是为了做区分。
往期回顾
- ahooks 源码解读系列
- ahooks 源码解读系列 - 2
- ahooks 源码解读系列 - 3
- ahooks 源码解读系列 - 4
- ahooks 源码解读系列 - 5
- ahooks 源码解读系列 - 6
- ahooks 源码解读系列 - 7
- ahooks 源码解读系列 - 8
- ahooks 源码解读系列 - 9
- ahooks 源码解读系列 - 10
今天是 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;
});
}
以上内容由于本人水平问题难免有误,欢迎大家进行讨论反馈。