ahooks源码系列(四):state 相关的 hook

522 阅读7分钟

可变数据和不可变数据

在开始 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> 的对象,里面有 togglesetsetLeftsetRight,优先级从左到右依次降低

然后这几个方法:

  • toggle:就是判断当前应该返回 defaultValue 还是 reverseValue
  • set:就是更改 state
  • setLeft:修改 state 为 defaultValue
  • setRight:修改 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.currentstate 是否相同,也就是对比当前的状态有没有发生改变,如果发生了改变,就记录变更前的状态: 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;

结语

以上内容如有错误,欢迎留言指出,一起进步💪,也欢迎大家一起讨论。