【解读 ahooks 源码系列】LifeCycle 篇 与 Scene 篇(一)
前言
本文是 ahooks 源码(v3.7.4)系列的第十二篇——LifeCycle 篇 与 Scene 篇(一)
往期文章:
- 【解读 ahooks 源码系列】(开篇)如何获取和监听 DOM 元素:useEffectWithTarget
- 【解读 ahooks 源码系列】DOM 篇(一):useEventListener、useClickAway、useDocumentVisibility、useDrop、useDrag
- 【解读 ahooks 源码系列】DOM 篇(二):useEventTarget、useExternal、useTitle、useFavicon、useFullscreen、useHover
- 【解读 ahooks 源码系列】DOM 篇(三):useMutationObserver、useInViewport、useKeyPress、useLongPress
- 【解读 ahooks 源码系列】DOM 篇(四):useMouse、useResponsive、useScroll、useSize、useFocusWithin
- 【解读 ahooks 源码系列】Dev 篇——useTrackedEffect 和 useWhyDidYouUpdate
- 【解读 ahooks 源码系列】Advanced 篇:useControllableValue、useCreation、useIsomorphicLayoutEffect、useEventEmitter、useLatest、useMemoizedFn、useReactive
- 【解读 ahooks 源码系列】State 篇(一):useSetState、useToggle、useBoolean、useCookieState、useLocalStorageState、useSessionStorageState、useDebounce、useThrottle
- 【解读 ahooks 源码系列】State 篇(二):useMap、useSet、usePrevious、useRafState、useSafeState、useGetState、useResetState
- 【解读 ahooks 源码系列】Effect 篇(一):useUpdateEffect、useUpdateLayoutEffect、useAsyncEffect、useDebounceFn、useDebounceEffect、useThrottleFn、useThrottleEffect
- 【解读 ahooks 源码系列】Effect 篇(二):useDeepCompareEffect、useDeepCompareLayoutEffect、useInterval、useTimeout、useRafInterval、useRafTimeout、useLockFn、useUpdate、useThrottleEffect
本文主要解读 useMount
、useUnmount
、useUnmountedRef
、useCounter
、useNetwork
、useSelections
、useHistoryTravel
的源码实现
LifeCycle 篇的三个 Hook 都很简单,都是看了名字和 Demo 基本就知道怎么实现的,所以不单独抽离篇章了。
useMount
只在组件初始化时执行的 Hook。
基本用法
import { useMount, useBoolean } from 'ahooks';
import { message } from 'antd';
import React from 'react';
const MyComponent = () => {
useMount(() => {
message.info('mount');
});
return <div>Hello World</div>;
};
export default () => {
const [state, { toggle }] = useBoolean(false);
return (
<>
<button type="button" onClick={toggle}>
{state ? 'unmount' : 'mount'}
</button>
{state && <MyComponent />}
</>
);
};
核心实现
实现就只是在 useEffect 封装了第一个参数回调(依赖为空数组)
const useMount = (fn: () => void) => {
useEffect(() => {
fn?.();
}, []);
};
useUnmount
在组件卸载(unmount)时执行的 Hook。
基本用法
import { useBoolean, useUnmount } from 'ahooks';
import { message } from 'antd';
import React from 'react';
const MyComponent = () => {
useUnmount(() => {
message.info('unmount');
});
return <p>Hello World!</p>;
};
export default () => {
const [state, { toggle }] = useBoolean(true);
return (
<>
<button type="button" onClick={toggle}>
{state ? 'unmount' : 'mount'}
</button>
{state && <MyComponent />}
</>
);
};
核心实现
实现就只是在 useEffect 的返回值中执行传入的函数
const useUnmount = (fn: () => void) => {
const fnRef = useLatest(fn);
useEffect(
() => () => {
fnRef.current();
},
[],
);
};
useUnmountedRef
获取当前组件是否已经卸载的 Hook。
基本用法
import { useBoolean, useUnmountedRef } from 'ahooks';
import { message } from 'antd';
import React, { useEffect } from 'react';
const MyComponent = () => {
const unmountedRef = useUnmountedRef();
useEffect(() => {
setTimeout(() => {
if (!unmountedRef.current) {
message.info('component is alive');
}
}, 3000);
}, []);
return <p>Hello World!</p>;
};
export default () => {
const [state, { toggle }] = useBoolean(true);
return (
<>
<button type="button" onClick={toggle}>
{state ? 'unmount' : 'mount'}
</button>
{state && <MyComponent />}
</>
);
};
核心实现
实现原理:通过判断有无执行 useEffect 中的返回值来判断组件是否已卸载。
const useUnmountedRef = () => {
const unmountedRef = useRef(false);
useEffect(() => {
unmountedRef.current = false;
return () => {
// 组件卸载
unmountedRef.current = true;
};
}, []);
return unmountedRef;
};
以下是 Scene 篇:
useCounter
管理计数器的 Hook。
基本用法
简单的 counter 管理示例。
import React from 'react';
import { useCounter } from 'ahooks';
export default () => {
const [current, { inc, dec, set, reset }] = useCounter(100, { min: 1, max: 10 });
return (
<div>
<p>{current} [max: 10; min: 1;]</p>
<div>
<button
type="button"
onClick={() => {
inc();
}}
style={{ marginRight: 8 }}
>
inc()
</button>
<button
type="button"
onClick={() => {
dec();
}}
style={{ marginRight: 8 }}
>
dec()
</button>
<button
type="button"
onClick={() => {
set(3);
}}
style={{ marginRight: 8 }}
>
set(3)
</button>
<button type="button" onClick={reset} style={{ marginRight: 8 }}>
reset()
</button>
</div>
</div>
);
};
核心实现
这个 hooks 的实现不难,其实就是内部封装暴露相应方法对数值进行管理。
function useCounter(initialValue: number = 0, options: Options = {}) {
// 获取外部传入的最小值与最大值
const { min, max } = options;
const [current, setCurrent] = useState(() => {
return getTargetValue(initialValue, {
min,
max,
});
});
// 设置值(支持传入 number 类型或函数)
const setValue = (value: ValueParam) => {
setCurrent((c) => {
const target = isNumber(value) ? value : value(c);
return getTargetValue(target, {
max,
min,
});
});
};
// 增加数值(默认加1)
const inc = (delta: number = 1) => {
setValue((c) => c + delta);
};
// 减少数值(默认减1)
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;
}
接下来重点看看 getTargetValue
的实现,该方法利用 Math.min
和 Math.max
进行取值,最终保证返回的值范围是大于等于 min,小于等于 max。
// 获取目标数值
function getTargetValue(val: number, options: Options = {}) {
const { min, max } = options;
let target = val;
if (isNumber(max)) {
// 取小于等于 max 的值
target = Math.min(max, target);
}
if (isNumber(min)) {
// 取大于等于 min 的值
target = Math.max(min, target);
}
return target;
}
useNetwork
管理网络连接状态的 Hook。
基本用法
返回网络状态信息
import React from 'react';
import { useNetwork } from 'ahooks';
export default () => {
const networkState = useNetwork();
return (
<div>
<div>Network information: </div>
<pre>{JSON.stringify(networkState, null, 2)}</pre>
</div>
);
};
核心实现
浏览器事件:
- online:浏览器在线工作时,online 事件被触发
- offline:当浏览器失去网络连接时,offline 事件被触发
- Network Change Event:监听网络连接状态
该 Hook 用到的是 NetworkInformation API
实现思路:监听 online
、offline
、navigator.connection.onchange
三个事件来处理逻辑,如自定义 state 变量 online
(网络是否为在线) 和 since
(online 最后改变时间),在事件回调改变值
function useNetwork(): NetworkState {
const [state, setState] = useState(() => {
return {
since: undefined,
online: navigator?.onLine,
...getConnectionProperty(),
};
});
useEffect(() => {
// 在线,设置 online 为 true
const onOnline = () => {
setState((prevState) => ({
...prevState,
online: true,
since: new Date(), // 记录最后一次更新的时间
}));
};
// 离线,设置 online 为 false
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;
}
getConnectionProperty
方法的实现,实际就是取 nav.connection
属性,然后获取里面属性,暴露出来
// 获取网络状态
function getConnection() {
const nav = navigator as any;
if (!isObject(nav)) 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,
};
}
useSelections
常见联动 Checkbox 逻辑封装,支持多选,单选,全选逻辑,还提供了是否选择,是否全选,是否半选的状态。
基本用法
import { Checkbox, Col, Row } from 'antd';
import React, { useMemo, useState } from 'react';
import { useSelections } from 'ahooks';
export default () => {
const [hideOdd, setHideOdd] = useState(false);
const list = useMemo(() => {
if (hideOdd) {
return [2, 4, 6, 8];
}
return [1, 2, 3, 4, 5, 6, 7, 8];
}, [hideOdd]);
const { selected, allSelected, isSelected, toggle, toggleAll, partiallySelected } = useSelections(
list,
[1],
);
return (
<div>
<div>Selected : {selected.join(',')}</div>
<div style={{ borderBottom: '1px solid #E9E9E9', padding: '10px 0' }}>
<Checkbox checked={allSelected} onClick={toggleAll} indeterminate={partiallySelected}>
Check all
</Checkbox>
<Checkbox checked={hideOdd} onClick={() => setHideOdd((v) => !v)}>
Hide Odd
</Checkbox>
</div>
<Row style={{ padding: '10px 0' }}>
{list.map((o) => (
<Col span={12} key={o}>
<Checkbox checked={isSelected(o)} onClick={() => toggle(o)}>
{o}
</Checkbox>
</Col>
))}
</Row>
</div>
);
};
核心实现
这个实现主要是结合 Set 结构使用数组来存储和操作数据,封装后暴露方法
function useSelections<T>(items: T[], defaultSelected: T[] = []) {
const [selected, setSelected] = useState<T[]>(defaultSelected);
const selectedSet = useMemo(() => new Set(selected), [selected]);
// 判断是否选中
const isSelected = (item: T) => selectedSet.has(item);
// 添加选中项到数组
const select = (item: T) => {
selectedSet.add(item);
// 这里需要使用 Array.from 将 Set 结构转为数组再存储
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));
};
// 是否一个都没有选择
const noneSelected = useMemo(() => items.every((o) => !selectedSet.has(o)), [items, selectedSet]);
// 是否全选
const allSelected = useMemo(
() => items.every((o) => selectedSet.has(o)) && !noneSelected,
[items, selectedSet, noneSelected],
);
// 是否半选
const partiallySelected = useMemo(
() => !noneSelected && !allSelected,
[noneSelected, allSelected],
);
// 反选全部元素
const toggleAll = () => (allSelected ? unSelectAll() : selectAll());
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;
useHistoryTravel
管理状态历史变化记录,方便在历史记录中前进与后退。
基本用法
import { useHistoryTravel } from 'ahooks';
import React from 'react';
export default () => {
const { value, setValue, backLength, forwardLength, back, forward } = useHistoryTravel<string>();
return (
<div>
<input value={value || ''} onChange={(e) => setValue(e.target.value)} />
<button disabled={backLength <= 0} onClick={back} style={{ margin: '0 8px' }}>
back
</button>
<button disabled={forwardLength <= 0} onClick={forward}>
forward
</button>
</div>
);
};
核心实现
实现思路:通过队列的方式维护过去和未来的队列,实现了两个工具函数 dumpIndex
和 split
function useHistoryTravel<T>(initialValue?: T, maxLength: number = 0) {
const [history, setHistory] = useState<IData<T | undefined>>({
present: initialValue, // 当前值
past: [], // 可回退历史队列
future: [], // 可前进历史队列
});
const { present, past, future } = history;
const initialValueRef = useRef(initialValue);
// 重置
const reset = (...params: any[]) => {
const _initial = params.length > 0 ? params[0] : initialValueRef.current;
initialValueRef.current = _initial;
setHistory({
present: _initial, // 重置到初始值或提供一个新的初始值
future: [],
past: [],
});
};
// 设置 value 值,都是往可回退的队列里添加值
const updateValue = (val: T) => {
const _past = [...past, present];
const maxLengthNum = isNumber(maxLength) ? maxLength : Number(maxLength);
// 有传历史记录最大长度 && 可回退历史长度大于最大长度
if (maxLengthNum > 0 && _past.length > maxLengthNum) {
// 删除第一个记录
_past.splice(0, 1);
}
setHistory({
present: val,
future: [], // 置空可前进历史队列
past: _past,
});
};
// 前进,默认前进一步(调用 split 函数,第二个参数传 future)
const _forward = (step: number = 1) => {
if (future.length === 0) {
return;
}
const { _before, _current, _after } = split(step, future);
setHistory({
// 旧状态,加上现在以及刚过去的
past: [...past, present, ..._before],
present: _current,
future: _after,
});
};
// 后退,默认后退一步(调用 split 函数,第二个参数传 past
const _backward = (step: number = -1) => {
if (past.length === 0) {
return;
}
const { _before, _current, _after } = split(step, past);
setHistory({
past: _before,
present: _current,
future: [..._after, present, ...future],
});
};
// 前进步数
const go = (step: number) => {
const stepNum = isNumber(step) ? step : Number(step);
if (stepNum === 0) {
return;
}
if (stepNum > 0) {
return _forward(stepNum);
}
_backward(stepNum);
};
return {
value: present, // 当前值
backLength: past.length, // 可回退历史长度
forwardLength: future.length, // 可前进历史长度
setValue: useMemoizedFn(updateValue), // 设置 value
go: useMemoizedFn(go), // 前进步数, step < 0 为后退, step > 0 时为前进
back: useMemoizedFn(() => {
go(-1); // 向后回退一步
}),
forward: useMemoizedFn(() => {
go(1); // 向前前进一步
}),
reset: useMemoizedFn(reset), // 重置到初始值,或提供一个新的初始值
};
}
dumpIndex
:计算一个数组中的索引值,可以向前或向后移动指定的步数,并确保返回的索引值在数组的索引范围内
- 正数:step > 0,index = step - 1 (数组的索引值从 0 开始)
- 负数:step < 0 ,index = arr.length + step
- 边界限制: 0 <= index <= arr.length - 1
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;
};
split
:根据传入的 targetArr、step,返回当前、之前、未来状态的队列
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),
};
};
转载自:https://juejin.cn/post/7243393786469810233