可变数据和不可变数据
在开始 state 相关的 ahook 学习之前,我们先来了解一下可变数据和不可变数据
-
可变数据:举个例子let objA = { name: '小明' }; let objB = objA; objB.name = '小红'; console.log(objA.name); // objA 的name也变成了小红当我们更改
objB.name时,发现objA.name也改变了,这种就是可变数据 -
不可变数据: 不可变数据的概念源自函数式编程,对于已经初始化的变量是不可以修改的。如果你要在这个初始化变量的基础上做一些更改,你必须创建一个新的变量,且你修改数据时不能影响之前的数据。这就是不可变数据
在 React 中,对于函数式组件来说,state 是不可变的数据,比如我们通常会这样去修改数组类型的 state
const [arr, setArr] = useState<Array<number>>([1,2,3]);
<Button
onClick={() => {
setArr(
arr.reduce(
(prev: Array<number>, item: number) => [...prev, item * 2],
[],
),
);
> 修改数组 </Button>
每次点击按钮修改数组时,其实都会返回一个新的数组,且每次修改不会影响 arr 这个已经初始化的 state
那为什么 React 中的 state 都是不可变数据呢?
是因为这样设计可以加速 diff 算法中
reconcile(调和)的过程,React 只需要检查object类型数据的地址有没有变即可确定数据有没有变。比如像memo就是浅比较新旧 props,只要地址没变,就不用重新渲染被包裹的组件
还有在每次通过 setState 去更改 object 类型的数据时,通常都会通过 ... 去解构他们
setObj(prev => ({
...prev,
name: 'Joylne',
props: {
...prev.props,
age: 20,
}
}));
每次这样编写可能会比较麻烦,且当这个 object 类型的数据层级比较深时,要多次 ...,所以 ahooks 帮我们封装了一些有关 state 的 hook
useSetState
useSetState 是管理 object 类型 state 的 Hooks,用法与 class 组件的 this.setState 基本一致。
源码如下:
export type SetState<S extends Record<string, any>> = <K extends keyof S>(
state: Pick<S, K> | null | ((prevState: Readonly<S>) => Pick<S, K> | S | null),
) => void;
const useSetState = <S extends Record<string, any>>(
initialState: S | (() => S),
): [S, SetState<S>] => {
const [state, setState] = useState<S>(initialState);
const setMergeState = useCallback((patch) => {
setState((prevState) => {
const newState = isFunction(patch) ? patch(prevState) : patch;
return newState ? { ...prevState, ...newState } : prevState;
});
}, []);
return [state, setMergeState];
};
export default useSetState;
它的实现思路也不算复杂:
- 首先,内部通过
useState保存初始值initialState,它和 useState 接受的参数类型一致,可以是一个确定的值,也可以是一个返回确定值的函数 - 然后内部创建一个
setMergeState的方法用于合并上一次的 state 和本次新的 state。在该方法内部,会先判断patch是否是一个函数,如果是则调用它并且传入上一次的状态prevState作为参数,输出新的状态,否则就直接作为新的状态。然后使用对象的拓展运算符,返回新对象,保证原有数据不变(React 中的 state 是不可变数据)
可以看到,其实就是将对象拓展运算符的操作封装到内部。
useToggle
useToggle 用于在两个状态值间切换的 Hook。
export interface Actions<T> {
setLeft: () => void;
setRight: () => void;
set: (value: T) => void;
toggle: () => void;
}
// 1
function useToggle<T = boolean>(): [boolean, Actions<T>];
// 2
function useToggle<T>(defaultValue: T): [T, Actions<T>];
// 3
function useToggle<T, U>(defaultValue: T, reverseValue: U): [T | U, Actions<T | U>];
// 4
function useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {
const [state, setState] = useState<D | R>(defaultValue);
const actions = useMemo(() => {
const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;
const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
const set = (value: D | R) => setState(value);
const setLeft = () => setState(defaultValue);
const setRight = () => setState(reverseValueOrigin);
return {
toggle,
set,
setLeft,
setRight,
};
// useToggle ignore value change
// }, [defaultValue, reverseValue]);
}, []);
return [state, actions];
}
export default useToggle;
它其实是通过 TS 函数重载,声明不同的入参返回不同的结果
比如 1 的入参是 boolean 类型,返回的值肯定就是对 boolean 类型的值取反,2 就是给 T 泛型返回的就是 T 泛型
3 的入参有两个,一个 defaultValue(传入默认的状态值),一个 reverseValue(传入取反的状态值),也就是切换这两个值的展示
4 的入参就是处理了一下 reverseValue,如果 reverseValue 没有传,那取反的值就直接取 !defaultValue
然后我们来看看他的实现逻辑
- 内部通过
useState存储defaultValue - 然后处理了一下返回值
reverseValue,如果没传就取!defaultValue - 然后返回一个
Actions<T>的对象,里面有toggle、set、setLeft、setRight,优先级从左到右依次降低
然后这几个方法:
toggle:就是判断当前应该返回defaultValue还是reverseValueset:就是更改statesetLeft:修改 state 为defaultValuesetRight:修改 state 为reverseValue
实现也挺易懂的
useBoolean
useBoolean 其实是 useToggle 的一种应用场景罢了
export interface Actions {
setTrue: () => void;
setFalse: () => void;
set: (value: boolean) => void;
toggle: () => void;
}
export default function useBoolean(defaultValue = false): [boolean, Actions] {
const [state, { toggle, set }] = useToggle(!!defaultValue);
const actions: Actions = useMemo(() => {
const setTrue = () => set(true);
const setFalse = () => set(false);
return {
toggle,
set: (v) => set(!!v),
setTrue,
setFalse,
};
}, []);
return [state, actions];
}
就是内部使用 useToggle 初始值是布尔类型的场景,然后返回 actions,里面还是那几个方法,很简单
来看看官网上 useBoolean 的使用:
import React from 'react';
import { useBoolean } from 'ahooks';
export default () => {
const [state, { toggle, setTrue, setFalse }] = useBoolean(true);
return (
<div>
<p>Effects:{JSON.stringify(state)}</p>
<p>
<button type="button" onClick={toggle}>
Toggle
</button>
<button type="button" onClick={setFalse} style={{ margin: '0 16px' }}>
Set false
</button>
<button type="button" onClick={setTrue}>
Set true
</button>
</p>
</div>
);
};
usePrevious
usePrevious 是保存上一次状态的 Hook。
举个例子:
export default () => {
const [count, setCount] = useState(0);
const previous = usePrevious(count);
return (
<div>
{count}
{previous}
<button onClick={() => setCount(prev => prev + 1)}>
更改 count
</button>
</div>
)
}
count 初始值是 0,当我点击按钮修改 count 时,count 为 1,而 previous 为 0,也就是上一次 count 的状态
源码如下:
import { useRef } from 'react';
export type ShouldUpdateFunc<T> = (prev: T | undefined, next: T) => boolean;
const defaultShouldUpdate = <T>(a?: T, b?: T) => !Object.is(a, b);
function usePrevious<T>(
state: T,
shouldUpdate: ShouldUpdateFunc<T> = defaultShouldUpdate,
): T | undefined {
const prevRef = useRef<T>();
const curRef = useRef<T>();
if (shouldUpdate(curRef.current, state)) {
prevRef.current = curRef.current;
curRef.current = state;
}
return prevRef.current;
}
export default usePrevious;
它的思路:
- 通过
prevRef存储上一次的状态,curRef存储最新的状态 - 内部有一个
shouldUpdate函数,这个函数默认用Object.is来比较curRef.current和state是否相同,也就是对比当前的状态有没有发生改变,如果发生了改变,就记录变更前的状态:prevRef.current = curRef.current,然后记录最新的状态:curRef.current = state - 然后返回上一次的状态
prevRef.current
还是挺简单的
useRafState
useRafState 是用来性能优化的,它只在 requestAnimationFrame callback 时更新 state
window.requestAnimationFrame() 方法传入一个 callback,它会在浏览器下一次重绘之前执行。假如你的操作是比较频繁的,就可以通过这个 hook 进行性能优化。
function useRafState<S>(initialState?: S | (() => S)) {
const ref = useRef(0);
const [state, setState] = useState(initialState);
const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
cancelAnimationFrame(ref.current);
ref.current = requestAnimationFrame(() => {
setState(value);
});
}, []);
useUnmount(() => {
cancelAnimationFrame(ref.current);
});
return [state, setRafState] as const;
}
export default useRafState;
它内部有个 setRafState 函数,该函数通过 requestAnimationFrame 去执行 setState,它执行的时候,会取消上一次的 setRafState 操作。然后重新通过 requestAnimationFrame 去控制本次 setState 的执行时机。然后在页面卸载的时候(useUnmount)的时候,也会取消操作。这样是为了防止内存泄漏(和定时器一个道理)
这个 hook 的实现简单,但是能想到通过 requestAnimationFrame 做性能优化我觉得很厉害,加我来写我肯定不会往这方面想🤒
useSafeState
useSafeState 用法与 React.useState 完全一样,但是在组件卸载后异步回调内的 setState 不再执行,避免因组件卸载后更新状态而导致的内存泄漏。
举个例子,如果组件内部有一个 setTimeout 异步的回调(过 5 秒执行),但是我在 5秒内 把组件卸载了,那么就不会去执行这个 setTimeout 异步回调了
这是官网的例子,简单易懂:
import { useSafeState } from 'ahooks';
import React, { useEffect, useState } from 'react';
const Child = () => {
const [value, setValue] = useSafeState<string>();
useEffect(() => {
setTimeout(() => {
setValue('data loaded from server');
}, 5000);
}, []);
const text = value || 'Loading...';
return <div>{text}</div>;
};
export default () => {
const [visible, setVisible] = useState(true);
return (
<div>
<button onClick={() => setVisible(false)}>Unmount</button>
{visible && <Child />}
</div>
);
};
我们来看看它的源码
function useSafeState<S>(initialState?: S | (() => S)) {
//1、标记组件是否卸载
const unmountedRef = useUnmountedRef();
//2、记录初始值
const [state, setState] = useState(initialState);
const setCurrentState = useCallback((currentState) => {
//3、如果组件卸载了,直接 return,不执行 setState
if (unmountedRef.current) return;
setState(currentState);
}, []);
return [state, setCurrentState] as const;
}
export default useSafeState;
这个 hook 也挺简单的:
- 在每次更新的时候,通过
useUnmountedRef来判断组件是否卸载 - 如果卸载了,直接 return,不执行 setState 更新状态
其中 useUnmountedRef 在我上一篇文章中提到过 # ahooks源码系列(三):LifeCycle、控制时机的 hook,这里再贴下代码:
const useUnmountedRef = () => {
const unmountedRef = useRef(false);
useEffect(() => {
unmountedRef.current = false;
return () => {
//组件卸载时,会记录 unmountRef.current = false;
unmountedRef.current = true;
};
}, []);
return unmountedRef;
};
export default useUnmountedRef;
useGetState
useGetState 给 React.useState 增加了一个 getter 方法,以获取当前最新值。
源码如下:
function useGetState<S>(initialState?: S) {
const [state, setState] = useState(initialState);
const stateRef = useRef(state);
stateRef.current = state;
const getState = useCallback(() => stateRef.current, []);
return [state, setState, getState];
}
export default useGetState;
其实就是通过 useRef 记录最新的 state 的值,并暴露一个 getState 方法获取到最新的状态。
其实也可以改造一下,使用 ahooks 里面的 useLatest,这个 hook 返回的是最新的值
function useGetState<S>(initialState?: S) {
const [state, setState] = useState(initialState);
const latestRef = useLatest(state);
const getState = useCallback(() => latestRef.current, []);
return [state, setState, getState];
}
export default useGetState;
结语
以上内容如有错误,欢迎留言指出,一起进步💪,也欢迎大家一起讨论。