ahooks源码解析 提高你写自定义hook的能力

1,335 阅读6分钟

ahooks源码解析 提高你写自定义hook的能力

前言:ahooks, 是阿里开源的一套高质量可靠的 React Hooks 库。由于想提高一下写自定义hooks的能力,所以去看了一下ahooks的实现。

比较通用的hook

useMemoizedFn

持久化 function 的 Hook,功能就是保持返回函数的引用不变,并使得回调始终为最新

function useMemoizedFn<T extends noop>(fn: T) {
​
  const fnRef = useRef<T>(fn);
​
  // why not write `fnRef.current = fn`?
  // https://github.com/alibaba/hooks/issues/728
    //这能保证fn为最新的,每次更新组件,传入最新的fn,同时更新fnRef
  fnRef.current = useMemo(() => fn, [fn]);
​
  const memoizedFn = useRef<PickFunction<T>>();
    //如果没有值,则把函数的,如果有则跳过,保证memoizedFn不会被重复赋值,保持最初的函数引用
  if (!memoizedFn.current) {
    
    memoizedFn.current = function (this, ...args) {
        //在最初的函数中调用fn,相当于做了一层代理
      return fnRef.current.apply(this, args);
    };
      
  }
  return memoizedFn.current as T;
}

通过 fnRef 保存最新的函数,然后通过memoizedFn 这个ref保持函数的引用不变, 应用场景

memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args);
    };
// 如果这个改成直接return fn.apply(),例:
memoizedFn.current = function (this, ...args) {
      return fn.apply(this,args)
    };
//这样会使得fn不是最新的,每次函数组件执行,传入最新的fn,都会因为前面的if (!memoizedFn.current) 判断而跳过赋值,所以需要用fnRef作为中转

useUpdateEffect

useUpdateEffect 用法等同于 useEffect,但是会忽略首次执行,只在依赖更新时执行。 typescript

const useUpdateEffect=createUpdateEffect(useEffect);

useUpdateLayoutEffect

useUpdateLayoutEffect 用法等同于 useLayoutEffect,但是会忽略首次执行,只在依赖更新时执行。

const useUpdateLayoutEffect= createUpdateEffect(useLayoutEffect);

上面这两个api都依赖createUpdateEffect

//createUpdateEffectexport const createUpdateEffect=(hook) => (effect, deps) => {
    
    const isMounted = useRef(false);
​
    // for react-refresh
    hook(() => {
      return () => {
        isMounted.current = false;
      };
    }, []);
​
    hook(() => {
      if (!isMounted.current) {
        isMounted.current = true;
      } else {
        return effect();
      }
    }, deps);
  };

isMounted 来判断是否是第一次调用effect 的回调,第一次不执行回调,更新isMounted状态。后续再执行回调

LifeCycle

useMount

只在组件初始化时执行的 Hook。

const useMount = (fn: () => void) => {
  useEffect(() => {
    fn?.();
  }, []);
};

通过useEffect ,dep传空数组,实现只调用一次。

`useEffect初次执行时,会走mountEffect方法,useEffect会给组件的fiber打上标记,当组件在beforeMutation 阶段,通过scheduleCallback异步调用flushPassiveEffects,进而调度useEffect回调。

而当组件更新时,走的是updateEffect方法,这个方法内会比较遍历dep,新旧dep不一致时才会更新,当传入空数组,则直接跳过遍历,不更新。

注意!在react 18 , createRoot创建严格模式下,当依赖项为零时, useEffect在严格模式下被调用两次。

回调执行:create => destory =>create

Strict Effects 会先挂载组件,再销毁组件,然后再挂载组件。以确保清理实际上清理了效果并且不会留下一些副作用。

问题讨论:Adding Reusable State to StrictMode

image.png

unmount

在组件卸载(unmount)时执行的 Hook。

const useUnmount = (fn: () => void) => {
 
    const ref = useRef(fn);
    ref.current = fn;
​
  useEffect(
    () => () => {
      fnRef.current();
    },
    [],
  );
};

为什么用 ref 挂载呢?是为了解决闭包产生的问题。

由于传的是空数组,当组件挂载后,回调执行,形成的 destroy 函数,挂载到effect对象上。此后,这个useEffect函数的回调不会再次执行,也就是这个destroy函数不会更新,可是外部传入的 fn 是会改变,每次函数组件执行,可能会生成一个新的 fn 。如果挂载fnref 中,则可以访问到最新的 fn ,因为 ref 的引用不会改变。

useUnmountedRef

获取当前组件是否已经卸载的 Hook。

const useUnmountedRef = () => {
  const unmountedRef = useRef(false);
  useEffect(() => {
    unmountedRef.current = false;
    return () => {
      unmountedRef.current = true;
    };
  }, []);
  return unmountedRef;
};

destroy 函数执行的时候,就把状态更改true,回调执行的时候就是更改 false

State

useSetState

管理 object 类型 state 的 Hooks,用法与 class 组件的 this.setState 基本一致。

const useSetState = (initialState)=>{
  const [state, setState] = useState(initialState);
    
  const setMergeState = useCallback((patch) => {
    setState((prevState) => {
      const newState = isFunction(patch) ? patch(prevState) : patch;
      return newState ? { ...prevState, ...newState } : prevState;
    });
  }, []);
​
  return [state, setMergeState];
};

setState在底层处理逻辑上主要是和老 state 进行合并处理,会有一层浅合并,而 useState 则是会重新赋值。

这个useSetState就是帮你做了一层合并

useToggle

用于在两个状态值间切换的 Hook。

function useToggle(defaultValue, reverseValue) {
  const [state, setState] = useState(defaultValue);
​
    //封装一个行为对象
  const actions = useMemo(() => {
    const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) 
​
    const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
    const set = (value) => setState(value);
    const setLeft = () => setState(defaultValue);
    const setRight = () => setState(reverseValueOrigin);
​
    return {
      toggle,
      set,
      setLeft,
      setRight,
    };
    // useToggle ignore value change
    // },[defaultValue, reverseValue]);
  }, []);
​
  return [state, actions];
}

注意!这个hook会忽略值的变化,因为 useMemo使用的依赖项为空数组

useBoolean

优雅的管理 boolean 状态的 Hook。

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];
}

这个hook基于useToggle

useCookieState

一个可以将状态存储在 Cookie 中的 Hook 。

import Cookies from 'js-cookie';
​
function useCookieState(cookieKey: string, options: Options = {}) {
​
  const [state, setState] = useState<State>(() => {
       //初始化的时候,从cookie取值
    const cookieValue = Cookies.get(cookieKey);
    if (isString(cookieValue)) return cookieValue;
​
    if (isFunction(options.defaultValue)) {
      return options.defaultValue();
    }
​
    return options.defaultValue;
  });
​
    //useMemoizedFn 功能就是保持返回函数的引用不变,并使得回调始终为最新
  const updateState = useMemoizedFn(
    (
      newValue: State | ((prevState: State) => State),
      newOptions: Cookies.CookieAttributes = {},
    ) => {
        //新旧的配置项合并取值,并setState回调中操作cookie的值
      const { defaultValue, ...restOptions } = { ...options, ...newOptions };
      setState((prevState) => {
        const value = isFunction(newValue) ? newValue(prevState) : newValue;
          
        if (value === undefined) {
          Cookies.remove(cookieKey);
        } else {
          Cookies.set(cookieKey, value, restOptions);
        }
        return value;
      });
    },
  );
​
  return [state, updateState] as const;
}

useLocalStorageState

将状态存储在 localStorage 中的 Hook 。

import isBrowser from '../utils/isBrowser';
const useLocalStorageState = createUseStorageState(() => (isBrowser ? localStorage : undefined));

useSessionStorageState

将状态存储在 sessionStorage 中的 Hook。

import isBrowser from '../utils/isBrowser';
​
const useSessionStorageState = createUseStorageState(() =>
  isBrowser ? sessionStorage : undefined,
);

这两个hook都基于 createUseStorageState 以及浏览器环境判断

浏览器环境判断

//浏览器环境判断,要有全局window变量,window变量有document属性,document属性有createElement属性
export const isUndef = (value: unknown): value is undefined => typeof value === 'undefined';
const isBrowser = !!(!isUndef(window) && window.document && window.document.createElement);
​

createUseStorageState

export function createUseStorageState(getStorage: () => Storage | undefined) {
    //根据不同的storage传值,返回这个hook函数
  function useStorageState<T>(key: string, options?: Options<T>) {
    let storage: Storage | undefined;
​
    // https://github.com/alibaba/hooks/issues/800
    try {
      storage = getStorage();
    } catch (err) {
      console.error(err);
    }
    
      //序列化函数
    const serializer = (value: T) => {
     //配置项有自定义的序列化函数就调用自定义的函数,没有就调用JSON.stringify进行序列化
      if (options?.serializer) {
        return options?.serializer(value);
      }
      return JSON.stringify(value);
    };
    //反序列化函数
    const deserializer = (value: string) => {
        //配置项有自定义的反序列化函数就调用自定义的函数,没有就调用JSON.parse解析
      if (options?.deserializer) {
        return options?.deserializer(value);
      }
      return JSON.parse(value);
    };
    //取值函数
    function getStoredValue() {
      try {
        const raw = storage?.getItem(key);
        //如果storage里这个value值,则进行反序列化
         if (raw) {
          return deserializer(raw);
        }
      } catch (e) {
        console.error(e);
      }
      if (isFunction(options?.defaultValue)) {
        return options?.defaultValue();
      }
      return options?.defaultValue;
    }
​
    const [state, setState] = useState<T | undefined>(() => getStoredValue());
​
    //这个hook,功能是只有依赖项更新时才会执行回调,会跳过第一次的初始化执行
    useUpdateEffect(() => {
        //当key改变的时候,同步改变state状态
      setState(getStoredValue());
    }, [key]);
​
    //更新状态函数
    const updateState = (value?: T | IFuncUpdater<T>) => {
      if (isUndef(value)) {
        setState(undefined);
        storage?.removeItem(key);
      } else if (isFunction(value)) {
        const currentState = value(state);
        try {
          setState(currentState);
          storage?.setItem(key, serializer(currentState));
        } catch (e) {
          console.error(e);
        }
      } else {
        try {
          setState(value);
          storage?.setItem(key, serializer(value));
        } catch (e) {
          console.error(e);
        }
      }
    };
    //useMemoizedFn下面讲
    return [state, useMemoizedFn(updateState)] as const;
  }
  return useStorageState;
}

注意!当浏览器完全禁用cookie时,使用storage会报错 : Failed to read the 'localStorage' property from 'Window': Access is denied for this document. 所以使用了try catch包裹 getStorage

当用户浏览器完全禁用cookie时,浏览器会禁用部分api,各个浏览器会有差异。

在这些浏览器中禁用 cookie 会禁用以下功能:

  • Chrome:cookies、localStorage、sessionStorage、IndexedDB
  • 火狐:cookies、localStorage、sessionStorage
  • IE:仅 cookie

详情请看:

禁用 cookie 时 useLocalStorageState 会抛出错误Can Session storage / local storage be disabled and Cookies enabled?

...hook比较多,后续补充