偷师ahooks 逐行源码解析🌟

让我们通过解读源码偷师ahooks的hooks设计并自练习实现,来理解并掌握 React Hooks、学习优秀的代码组织和架构设计 ,在日常开发中可以多借鉴使用。

Doc: ahooks - React Hooks Library - ahooks 3.0

会把除一些特殊场景的hooks的其他hooks都过一遍

ahooks做了什么

ahooks是一套高质量可靠的 React Hooks 库。在当前 React 项目研发过程中,一套好用的 React Hooks 库是必不可少的,我们经常会在很多场景下需要使用到一些复杂的hooks,ahooks往往会成为首选,他具备以下特性

特性

  • 易学易用
  • 支持 SSR
  • 对输入输出函数做了特殊处理,且避免闭包问题
  • 包含大量提炼自业务的高级 Hooks
  • 包含丰富的基础 Hooks
  • 使用 TypeScript 构建,提供完整的类型定义文件

所以ahooks就是hooks集,并按照 Hooks 的功能和应用场景来进行分类和划分。主要包括以下几个类别:

  1. Scene Hooks: 用于处理具体场景的 Hooks。
  2. Effect Hooks :主要用来处理带有副作用的操作,比如 useUpdateEffectuseTimeout 等。
  3. State Hooks :包含了一些管理和操作状态的 Hooks,例如 useBooleanuseCounter 等。
  4. Dom Hooks :主要用于操作 DOM 元素,例如 useClickAwayuseScroll 等。
  5. LifeCycle Hooks :用于处理组件的生命周期,如 useMountuseUnmount 等。
  6. Request Hooks :用于处理异步请求,如useRequest
  7. Advanced Hooks:更加精细和深入,用于处理更复杂的场景。
  8. Dev hooks: 在开发环境下执行的hooks,比如useWhyDidYouUpdate

下面我们也依照不同的分类来进行学习

LifeCycle Hooks

用于处理组件的生命周期

useMount

通过对useEffect指定响应式依赖项为空,实现只在组件初始化时执行fn。

php 复制代码
const useMount = (fn: () => void) => {
  useEffect(() => {
    fn?.();
  }, []);
};

useUnmount

同样设置useEffect的响应式依赖为空在组件卸载(unmount)时执行fn

ini 复制代码
const useUnmount = (fn: () => void) => {
  const fnRef = useLatest(fn);

  useEffect(
    () => () => {
      fnRef.current();
    },
    [],
  );
};

这里比较有趣的是

useLatest

,将 fn用useRef 存储,并每次渲染的时候更新fn起来避免闭包问题, 即返回的永远是最新值。

javascript 复制代码
function useLatest<T>(value: T) {
  const ref = useRef(value);
  ref.current = value;

  return ref;
}

// 使用场景
const { counter } = this.state;
// 创建一个 ref 来存储最新的 counter 值
const counterRef = useLatest(counter);
useEffect(() => {
  const timerId = setInterval(() => {
    console.log(counterRef.current); // 每次打印的都是最新的 counter 值
  }, 1000);
  return () => clearInterval(timerId);
}, [])
// 比如在其他的地方改变 counter
this.setState({ counter: counter + 1 });

对于

useLatest

,可以延伸到usePersistFn。usePersisFn在useLatest的基础上保留了函数的引用,永远只在首次渲染时生成了一个永不改变的函数引用,做到了持久化,避免了组件更新的时候需要重新创建函数。在很多场景下都可以适当使用 usePersistFn 来替代 useCallback,一般情况下fn可以都使用usePersistFn 进行封装。

ini 复制代码
function usePersistFn<T extends noop>(fn: T) {
  const fnRef = useRef<T>(fn);
  fnRef.current = fn;

  const persistFn = useRef<T>();
  if (!persistFn.current) {
    persistFn.current = function (...args) {
      // @ts-ignore this
      return fnRef.current.apply(this, args);
    } as T;
  }

  return persistFn.current!;
}

useUnmountedRef

获取当前组件是否已经卸载的 Hook,利用useEffect 组件销毁时执行的实现的逻辑。

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

State Hooks

包含了一些管理和操作状态的 Hooks

useSetState

管理 object 类型 state 的 Hooks,用法与 class 组件的 this.setState 基本一致,增加了自动合并对象

ini 复制代码
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];
};

useBoolean

优雅的管理 boolean 状态,用于处理usestate需设置为boolean的情况

dart 复制代码
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, // 切换state状态
      set: (v) => set(!!v), // 设置 state
      setTrue, // 设置为 true
      setFalse, // 设置为 false
    };
  }, []);

  return [state, actions];
}

useToggle

用于在两个状态值之间切换的 Hook,如false => true

ini 复制代码
function useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {
  // 初始化 state
  const [state, setState] = useState<D | R>(defaultValue);

  const actions = useMemo(() => {
    // 计算状态相反值 false => true
    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, // 反转state值
      set, // 设置state
      setLeft, // 设置state为默认值
      setRight, // 设置state为默认相反值
    };
  }, []);

  return [state, actions];
}

useUrlState

基于 react-router 的 useLocation & useHistory & useNavigate 将 query 信息转为state进行管理。

ini 复制代码
//packages/use-url-state/src/index.ts
import * as tmp from 'react-router';
const rc = tmp as any;
const useUrlState = <S extends UrlState = UrlState>(
  initialState?: S | (() => S),
  options?: Options,
) => {
  type State = Partial<{ [key in keyof S]: any }>;
  const { navigateMode = 'push', parseOptions, stringifyOptions } = options || {};

  const mergedParseOptions = { ...baseParseConfig, ...parseOptions };
  const mergedStringifyOptions = { ...baseStringifyConfig, ...stringifyOptions };

  const location = rc.useLocation();

  // react-router v5 rc export from 
  const history = rc.useHistory?.();
  // react-router v6
  const navigate = rc.useNavigate?.();

  const update = useUpdate();

  const initialStateRef = useRef(
    typeof initialState === 'function' ? (initialState as () => S)() : initialState || {},
  );

  const queryFromUrl = useMemo(() => {
    return qs.parse(location.search, mergedParseOptions);
  }, [location.search]);

  // 合并query
  const targetQuery: State = useMemo(
    () => ({
      ...initialStateRef.current,
      ...queryFromUrl,
    }),
    [queryFromUrl],
  );

  const setState = (s: React.SetStateAction<State>) => {
    const newQuery = typeof s === 'function' ? s(targetQuery) : s;
    // 1. 如果 setState 后,search 没变化,就需要 update 来触发一次更新。比如 demo1 直接点击 clear,就需要 update 来触发更新。
    // 2. update 和 history 的更新会合并,不会造成多次更新
    update(); //刷新一次组件
    // 区分react-router v5 or v6
    if (history) {
      // 更新 query
      history[navigateMode](
        {
          hash: location.hash,
          search: qs.stringify({ ...queryFromUrl, ...newQuery }, mergedStringifyOptions) || '?',
        },
        location.state,
      );
    }
    if (navigate) {
      // 更新 query
      navigate(
        {
          hash: location.hash,
          search: qs.stringify({ ...queryFromUrl, ...newQuery }, mergedStringifyOptions) || '?',
        },
        {
          replace: navigateMode === 'replace',
          state: location.state,
        },
      );
    }
  };

  return [targetQuery, useMemoizedFn(setState)] as const;
};

// useUpdate: 刷新一次组件
const useUpdate = () => {
  const [, setState] = useState({});

  return useCallback(() => setState({}), []);
};

useCookieState

将状态存储在 Cookie 中,并管理

ini 复制代码
function useCookieState(cookieKey: string, options: Options = {}) {
  const [state, setState] = useState<State>(() => {
    // 读cookie 初始化state
    const cookieValue = Cookies.get(cookieKey);

    if (isString(cookieValue)) return cookieValue;

    if (isFunction(options.defaultValue)) {
      return options.defaultValue();
    }

    return options.defaultValue;
  });
  
   // 更新state & cookie
   // useMemoizedFn 持久化fn同 usePersistFn 一样
  const updateState = useMemoizedFn(
    (
      newValue: State | ((prevState: State) => State),
      newOptions: Cookies.CookieAttributes = {},
    ) => {
      const { defaultValue, ...restOptions } = { ...options, ...newOptions };
      const value = isFunction(newValue) ? newValue(state) : newValue;

      setState(value);

      if (value === undefined) {
        Cookies.remove(cookieKey);
      } else {
        Cookies.set(cookieKey, value, restOptions);
      }
    },
  );

  return [state, updateState] as const;
}

useLocalStorageState

将localStorage里的信息转为state进行管理,大致设计和cookie一样多了序列化和反序列化的处理

scss 复制代码
// 判断是否为浏览器
const useLocalStorageState = createUseStorageState(() => (isBrowser ? localStorage : undefined));

export function createUseStorageState(getStorage: () => Storage | undefined) {
  function useStorageState<T>(key: string, options: Options<T> = {}) {
    let storage: Storage | undefined;
    const {
      onError = (e) => {
        console.error(e);
      },
    } = options;

    // https://github.com/alibaba/hooks/issues/800
    try {
      storage = getStorage();
    } catch (err) {
      onError(err);
    }
    // 序列化
    const serializer = (value: T) => {
      if (options.serializer) {
        return options.serializer(value);
      }
      return JSON.stringify(value);
    };
    // 反序列化
    const deserializer = (value: string): T => {
      if (options.deserializer) {
        return options.deserializer(value);
      }
      return JSON.parse(value);
    };
    // 获取store里的值
    function getStoredValue() {
      try {
        const raw = storage?.getItem(key);
        if (raw) {
          return deserializer(raw);
        }
      } catch (e) {
        onError(e);
      }
      if (isFunction(options.defaultValue)) {
        return options.defaultValue();
      }
      return options.defaultValue;
    }

    const [state, setState] = useState(getStoredValue);

    useUpdateEffect(() => {
      setState(getStoredValue());
    }, [key]);
    
    // 更新state
    const updateState = (value?: SetState<T>) => {
      const currentState = isFunction(value) ? value(state) : value;
      setState(currentState);

      if (isUndef(currentState)) {
        storage?.removeItem(key);
      } else {
        try {
          storage?.setItem(key, serializer(currentState));
        } catch (e) {
          console.error(e);
        }
      }
    };

    return [state, useMemoizedFn(updateState)] as const;
  }
  return useStorageState;
}

useSessionStorageState

同useLocalStorageState一样

useDebounce

用来处理防抖

用法:

ini 复制代码
  const debouncedValue = useDebounce(value, { wait: 500 });
scss 复制代码
function useDebounce<T>(value: T, options?: DebounceOptions) {
  const [debounced, setDebounced] = useState(value);

  const { run } = useDebounceFn(() => {
    setDebounced(value);
  }, options);

  useEffect(() => {
    run();
  }, [value]);

  return debounced;
}

// useDebounceFn
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
  // 获取最新fn
  const fnRef = useLatest(fn);
  // 执行步长
  const wait = options?.wait ?? 1000;
  
  const debounced = useMemo(
    () =>
      // lodash 提供的debounce(setTimeout)
      debounce(
        (...args: Parameters<T>): ReturnType<T> => {
          return fnRef.current(...args);
        },
        wait,
        options,
      ),
    [],
  );
  
  // 卸载时取消debounced(clearTimeout)
  useUnmount(() => {
    debounced.cancel();
  });

  return {
    run: debounced,
    cancel: debounced.cancel,
    flush: debounced.flush,
  };
}

useThrottle

处理节流,实现原理和useDebounce一样

scss 复制代码
function useThrottle<T>(value: T, options?: ThrottleOptions) {
  const [throttled, setThrottled] = useState(value);

  const { run } = useThrottleFn(() => {
    setThrottled(value);
  }, options);

  useEffect(() => {
    run();
  }, [value]);

  return throttled;
}
// useThrottleFn
function useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) {
  if (isDev) {
    if (!isFunction(fn)) {
      console.error(`useThrottleFn expected parameter is a function, got ${typeof fn}`);
    }
  }

  const fnRef = useLatest(fn);

  const wait = options?.wait ?? 1000;

  const throttled = useMemo(
    () =>
      throttle(
        (...args: Parameters<T>): ReturnType<T> => {
          return fnRef.current(...args);
        },
        wait,
        options,
      ),
    [],
  );

  useUnmount(() => {
    throttled.cancel();
  });

  return {
    run: throttled,
    cancel: throttled.cancel,
    flush: throttled.flush,
  };
}

useMap

管理 Map 类型状态,模拟Map的操作(setAll、remove、set、reset),并将每次修改更新到state

dart 复制代码
function useMap<K, T>(initialValue?: Iterable<readonly [K, T]>) {
  const getInitValue = () => new Map(initialValue);
  const [map, setMap] = useState<Map<K, T>>(getInitValue);

  const set = (key: K, entry: T) => {
    setMap((prev) => {
      const temp = new Map(prev);
      temp.set(key, entry);
      return temp;
    });
  };

  const setAll = (newMap: Iterable<readonly [K, T]>) => {
    setMap(new Map(newMap));
  };

  const remove = (key: K) => {
    setMap((prev) => {
      const temp = new Map(prev);
      temp.delete(key);
      return temp;
    });
  };

  const reset = () => setMap(getInitValue());

  const get = (key: K) => map.get(key);

  return [
    map,
    {
      set: useMemoizedFn(set),
      setAll: useMemoizedFn(setAll),
      remove: useMemoizedFn(remove),
      reset: useMemoizedFn(reset),
      get: useMemoizedFn(get),
    },
  ] as const;
}

useSet

同useMap一样

usePrevious

保存上一次状态

ini 复制代码
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;
}

useRafState

只在 requestAnimationFrame callback 时更新 state,一般用于性能优化,避免页面卡顿。可能会有以下使用场景

  1. 高频更新状态: 如果在 React 组件中有一个状态需要在短时间内更新很多次,对性能要求较高,例如拖拽操作、游戏、音视频播放、画布操作等,那么 useRafState 就会非常有用。
  2. 动画和过渡效果: 对于涉及到动画和过渡效果的场景,useRafState 可以更平滑地控制动作,提高用户体验。
  3. 复杂或大数据集的交互: 在处理稍微复杂或大量的数据,并且这些数据更改可能导致频繁更新状态和重绘的情况下,useRafState 可以优化性能,确保界面响应顺畅。
scss 复制代码
function useRafState<S>(initialState?: S | (() => S)) {
  // 通过ref记录AnimationFrame,卸载时清理
  const ref = useRef(0);
  const [state, setState] = useState(initialState);

  const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
    cancelAnimationFrame(ref.current);
    
    // 在requestAnimationFrame callback时才更新state
    ref.current = requestAnimationFrame(() => {
      setState(value);
    });
  }, []);

  useUnmount(() => {
    cancelAnimationFrame(ref.current);
  });

  return [state, setRafState] as const;
}

useSafeState

用法与 React.useState 完全一样,但是在组件卸载后异步回调内的 setState 不再执行,避免因组件卸载后更新状态而导致的内存泄漏。若有setState还没执行完就更新组件的情况下可以使用useSafeState。

scss 复制代码
function useSafeState<S>(initialState?: S | (() => S)) {
  const unmountedRef = useUnmountedRef();
  const [state, setState] = useState(initialState);
  const setCurrentState = useCallback((currentState) => {
    /** if component is unmounted, stop update */
    if (unmountedRef.current) return;
    setState(currentState);
  }, []);

  return [state, setCurrentState] as const;
}

useGetState

为setate添加一个get函数用于获取最新的state,本质就是将每次更新通过ref存储,ref永远返回的是最新的值。useState受闭包影响,当前环境下只能获取当前的state

ini 复制代码
function useGetState<S>(
  initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>, GetStateAction<S>];
function useGetState<S = undefined>(): [
  S | undefined,
  Dispatch<SetStateAction<S | undefined>>,
  GetStateAction<S | undefined>,
];
function useGetState<S>(initialState?: S) {
  const [state, setState] = useState(initialState);
  const stateRef = useLatest(state);

  const getState = useCallback(() => stateRef.current, []);

  return [state, setState, getState];
}

useResetState

提供重置 state 方法的 Hooks

ini 复制代码
const useResetState = <S>(
  initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>, ResetState] => {
  const [state, setState] = useState(initialState);
  // reset
  const resetState = useMemoizedFn(() => {
    setState(initialState);
  });

  return [state, setState, resetState];
};

Effect Hooks

主要用来处理带有副作用的操作

useUpdateEffect

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

ini 复制代码
 useUpdateEffect => createUpdateEffect(useEffect)
 export const createUpdateEffect: (hook: EffectHookType) => EffectHookType =
  (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);
  };

useUpdateLayoutEffect

原理同useUpdateEffect一样添加isMounted(useRef)的判断,跳过updateLayoutEffect的首次刷新

useAsyncEffect

支持异步函数。useEffect是不支持异步函数的,一般都是将异步函数在useEffect函数内进行封装,比如:

javascript 复制代码
 useEffect(() => {
    async function startFetching() {
      setBio(null);
      const result = await fetchBio(person);
      if (!ignore) {
        setBio(result);
      }
    }
    let ignore = false;
    startFetching();
    return () => {
      ignore = true;
    }
}, [person]);
// 使用useAsyncEffect
 useAsyncEffect(async () => {
    async function startFetching() {
      setBio(null);
      const result = await fetchBio(person);
      if (!ignore) {
        setBio(result);
      }
    }
    await startFetching(); 
    // 不需要再ignore
  }, [person]);

// useAsyncEffect
function useAsyncEffect(
  effect: () => AsyncGenerator<void, void, void> | Promise<void>,
  deps?: DependencyList,
) {
  useEffect(() => {
    // 执行effect
    const e = effect();
    let cancelled = false;
    // 
    async function execute() {
      // 是否为异步可迭代函数
      if (isAsyncGenerator(e)) {
        while (true) {
          const result = await e.next();
          if (result.done || cancelled) {
            break;
          }
        }
      } else {
        await e;
      }
    }
    execute();
    return () => {
      cancelled = true;
    };
  }, deps);
}
  

可见useAsyncEffect其实就是封装了异步函数,并处理了时序竞争的问题( 你的代码不会受到"竞争条件"的影响:网络响应可能会以与你发送的不同的顺序到达)。

Tips: 如果判断一个函数为异步可迭代函数

javascript 复制代码
 typeof fn[Symbol.asyncIterator] === 'function'

useDebounceEffect

useEffect 增加防抖的能力。将useEffect用useDebounceFn封装了,flag更新后才触发真的effect

scss 复制代码
function useDebounceEffect(
  effect: EffectCallback,
  deps?: DependencyList,
  options?: DebounceOptions,
) {
  const [flag, setFlag] = useState({});

  const { run } = useDebounceFn(() => {
    setFlag({});
  }, options);

  useEffect(() => {
    return run();
  }, deps);

  useUpdateEffect(effect, [flag]);
}

useThrottleEffect

原理和useDebounceEffect一样

useDeepCompareEffect

deps 通过 react-fast-compare 进行深比较。

ini 复制代码
useEffect(() => {
    effectCountRef.current += 1;
  }, [{}]);

useDeepCompareEffect(() => {
    deepCompareCountRef.current += 1;
    return () => {
      // do something
    };
}, [{}]);

useDeepCompareEffect => createDeepCompareEffect(useEffect)

export const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => {
  const ref = useRef<DependencyList>();
  const signalRef = useRef<number>(0);

  // 深比较,不一致才更改signalRef。触发effect
  if (deps === undefined || !depsEqual(deps, ref.current)) {
    ref.current = deps;
    signalRef.current += 1;
  }

  hook(effect, [signalRef.current]);
};

useDeepCompareLayoutEffect

原理与useDeepCompareEffect一样

useInterval

可以处理 setInterval 的 Hook

scss 复制代码
// 用例
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
// useInterval 
const useInterval = (fn: () => void, delay?: number, options: { immediate?: boolean } = {}) => {
  const timerCallback = useMemoizedFn(fn);
  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);

  const clear = useCallback(() => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
  }, []);

  useEffect(() => {
    if (!isNumber(delay) || delay < 0) {
      return;
    }
    if (options.immediate) {
      // 执行callback
      timerCallback();
    }
    timerRef.current = setInterval(timerCallback, delay);
    // 清除Interval
    return clear;
  }, [delay, options.immediate]);

  return clear;
};

useRafInterval

requestAnimationFrame 模拟实现 setInterval,API 和 useInterval 保持一致,好处是可以在页面不渲染的时候停止执行定时器,比如页面隐藏或最小化等。

useTimeout

可以处理 setTimeout 计时器函数的 Hook。实现方式同useInterval一样

useRafTimeout

requestAnimationFrame 模拟实现 useTimeout,实现原理和useRafInterval一样

useLockFn

用于给一个异步函数增加竞态锁,防止并发执行。

ini 复制代码
// 通过 lockRef 标记是否已执行
function useLockFn<P extends any[] = any[], V = any>(fn: (...args: P) => Promise<V>) {
  const lockRef = useRef(false);

  return useCallback(
    async (...args: P) => {
      if (lockRef.current) return;
      lockRef.current = true;
      try {
        const ret = await fn(...args);
        return ret;
      } catch (e) {
        throw e;
      } finally {
        lockRef.current = false;
      }
    },
    [fn],
  );
}

useUpdate

useUpdate 会返回一个函数,调用该函数会强制组件重新渲染。

scss 复制代码
const useUpdate = () => {
  const [, setState] = useState({});

  return useCallback(() => setState({}), []);
};

Dom Hooks

主要用于操作 DOM 元素

步骤

  1. 获取目标element
  2. 绑定hooks

每一个Dom hooks都需要使用到

useEffectWithTarget

去获取目标elements通过DOM 节点或者 ref后执行对应elements的effect

useEffectWithTarget🌟

ini 复制代码
const useEffectWithTarget = createEffectWithTarget(useEffect);
const createEffectWithTarget = (useEffectType: typeof useEffect | typeof useLayoutEffect) => {
  /**
   * @param effect
   * @param deps
   * @param target target should compare ref.current vs ref.current, dom vs dom, ()=>dom vs ()=>dom
   */
  const useEffectWithTarget = (
    effect: EffectCallback,
    deps: DependencyList,
    target: BasicTarget<any> | BasicTarget<any>[],
  ) => {
    // 是否初始化
    const hasInitRef = useRef(false);
    // 最后操作的元素
    const lastElementRef = useRef<(Element | null)[]>([]);
    // 依赖
    const lastDepsRef = useRef<DependencyList>([]);
    // 退出
    const unLoadRef = useRef<any>();
    useEffectType(() => {
      // 判断传入的目标 DOM 节点或者 ref
      const targets = Array.isArray(target) ? target : [target];
      // 获取Dom elements
      const els = targets.map((item) => getTargetElement(item));
    
      // init run
      if (!hasInitRef.current) {
        hasInitRef.current = true;
        lastElementRef.current = els;
        lastDepsRef.current = deps;

        unLoadRef.current = effect();
        return;
      }

      if (
        els.length !== lastElementRef.current.length ||
        !depsAreSame(lastElementRef.current, els) ||
        !depsAreSame(lastDepsRef.current, deps)
      ) {
        unLoadRef.current?.();

        lastElementRef.current = els;
        lastDepsRef.current = deps;
        unLoadRef.current = effect();
      }
    });

    useUnmount(() => {
      unLoadRef.current?.();
      // for react-refresh
      hasInitRef.current = false;
    });
  };

  return useEffectWithTarget;
};

// getTargetElement 
export function getTargetElement<T extends TargetType>(target: BasicTarget<T>, defaultElement?: T) {
  if (!isBrowser) {
    return undefined;
  }

  if (!target) {
    return defaultElement;
  }

  let targetElement: TargetValue<T>;

  if (isFunction(target)) {
    targetElement = target();
  } else if ('current' in target) {
    // ref
    targetElement = target.current;
  } else {
    // dom
    targetElement = target;
  }
  return targetElement;
}

useEventListener

优雅的使用addEventListener

scss 复制代码
function useEventListener(eventName: string, handler: noop, options: Options = {}) {
  // 获取最新的handler fn
  const handlerRef = useLatest(handler);

  useEffectWithTarget(
    () => {
      const targetElement = getTargetElement(options.target, window);
      if (!targetElement?.addEventListener) {
        return;
      }
      
      // 处理事件
      const eventListener = (event: Event) => {
        return handlerRef.current(event);
      };

      // 添加监听事件
      targetElement.addEventListener(eventName, eventListener, {
        capture: options.capture,
        once: options.once,
        passive: options.passive,
      });
      // clear
      return () => {
        targetElement.removeEventListener(eventName, eventListener, {
          capture: options.capture,
        });
      };
    },
    [eventName, options.capture, options.once, options.passive],
    options.target, // DOM 节点或者 ref
  );
}

useHover

监听 DOM 元素是否有鼠标悬停。

scss 复制代码
// 用法
const isHovering = useHover(ref);
// useHover
export default (target: BasicTarget, options?: Options): boolean => {
  const { onEnter, onLeave, onChange } = options || {};

  const [state, { setTrue, setFalse }] = useBoolean(false);

  useEventListener(
    'mouseenter',
    () => {
      onEnter?.();
      setTrue();
      onChange?.(true);
    },
    {
      target,
    },
  );

  useEventListener(
    'mouseleave',
    () => {
      onLeave?.();
      setFalse();
      onChange?.(false);
    },
    {
      target,
    },
  );

  return state;
};

其他的Hooks实现都大同小异就不再继续赘述

Advanced Hooks🌟

更加精细和深入的hooks,用于处理更复杂的场景。

useControllableValue

在某些组件开发时,我们需要组件的状态既可以自己管理,也可以被外部控制,useControllableValue 就是帮你管理这种状态的 Hook。

用例

typescript 复制代码
const ControllableComponent = (props: any) => {
  // 由父级控制 state
  const [state, setState] = useControllableValue<string>(props);
  return <input value={state} onChange={(e) => setState(e.target.value)} />;
};
ini 复制代码
function useControllableValue<T = any>(props: Props = {}, options: Options<T> = {}) {
  const {
    defaultValue,
    defaultValuePropName = 'defaultValue',
    valuePropName = 'value',
    trigger = 'onChange',
  } = options;

  const value = props[valuePropName] as T;
  // 是否prop中存在value
  // props 中没有 value,则组件内部自己管理 state
  const isControlled = props.hasOwnProperty(valuePropName);

  const initialValue = useMemo(() => {
    if (isControlled) {
      // 使用props的value init
      return value;
    }
    if (props.hasOwnProperty(defaultValuePropName)) {
      return props[defaultValuePropName];
    }
    return defaultValue;
  }, []);

  const stateRef = useRef(initialValue);
  if (isControlled) {
    stateRef.current = value;
  }

  const update = useUpdate();

  function setState(v: SetStateAction<T>, ...args: any[]) {
    // 新的state
    const r = isFunction(v) ? v(stateRef.current) : v;

    if (!isControlled) {
      stateRef.current = r;
      // 非props控制组件,需要每次setstate的时候刷新组件(模拟useState)
      update();
    }
    if (props[trigger]) {
      // 执行prop的seState 触发更新
      props[trigger](r, ...args);
    }
  }

  return [stateRef.current, useMemoizedFn(setState)] as const;
}

useCreation

  • 对比useRef 有更好的性能,不会出现每次重渲染都初始化useRef -- React 中文文档
  • 能保证memo了不会被重新计算

用法

scss 复制代码
const foo = useCreation(() => new Foo(), []);
javascript 复制代码
// 本质还是useref,但加上了一个模拟deps并每次重渲染的时候diff更新ref
export default function useCreation<T>(factory: () => T, deps: DependencyList) {
  const { current } = useRef({
    deps,
    obj: undefined as undefined | T,
    initialized: false,
  });
  // 判断是否更新ref
  if (current.initialized === false || !depsAreSame(current.deps, deps)) {
    current.deps = deps;
    current.obj = factory();
    current.initialized = true;
  }
  return current.obj as T;
}
// 深比较
export default function depsAreSame(oldDeps: DependencyList, deps: DependencyList): boolean {
  if (oldDeps === deps) return true;
  for (let i = 0; i < oldDeps.length; i++) {
    if (!Object.is(oldDeps[i], deps[i])) return false;
  }
  return true;
}

useEventEmitter

提供多个组件之间的事件通知(发布-订阅者)

用法

javascript 复制代码
const event$ = useEventEmitter();
// emit 
event$.emit('hello');
// Subscription
event$.useSubscription(val => {
  console.log(val);
});
ini 复制代码
export class EventEmitter<T> {
  private subscriptions = new Set<Subscription<T>>();
  
  // 给所有的订阅者发送消息
  emit = (val: T) => {
    for (const subscription of this.subscriptions) {
      subscription(val);
    }
  };

  useSubscription = (callback: Subscription<T>) => {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    const callbackRef = useRef<Subscription<T>>();
    callbackRef.current = callback;
    // eslint-disable-next-line react-hooks/rules-of-hooks
    useEffect(() => {
      function subscription(val: T) {
        if (callbackRef.current) {
          callbackRef.current(val);
        }
      }
      // 添加订阅者
      this.subscriptions.add(subscription);
      return () => {
        // 移除订阅者
        this.subscriptions.delete(subscription);
      };
    }, []);
  };
}

export default function useEventEmitter<T = void>() {
  const ref = useRef<EventEmitter<T>>();
  if (!ref.current) {
    // 创建event实例
    ref.current = new EventEmitter();
  }
  return ref.current;
}

useIsomorphicLayoutEffect

在 SSR 模式下,使用 useLayoutEffect 时会出现告警,所以在非浏览器环境下使用useEffectd代替useLayoutEffect

ini 复制代码
const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;

useLatest

返回当前最新值的 Hook,可以避免闭包问题。本质就是每次重渲染的时候都更新用于存储state都ref

csharp 复制代码
// 用法
const [count, setCount] = useState(0);
const latestCountRef = useLatest(count);
// useLatest
function useLatest<T>(value: T) {
  const ref = useRef(value);
  // 每次重渲染都执行
  ref.current = value;

  return ref;
}

useMemoizedFn

持久化 function 的 Hook,一般情况下,可以使用 useMemoizedFn 完全代替 useCallback,也都可以把fn通过useMemoizdFn封装

javascript 复制代码
// 用法'
const [state, setState] = useState('');

// func 地址永远不会变化
const func = useMemoizedFn(() => {
  console.log(state);
})
// useMemoizedFn
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
  // 每次重渲染时更新fnRef,因为memoizedFn.current不会再更新,所以需要每次都更新fnRef
  fnRef.current = useMemo<T>(() => fn, [fn]);
  
  // 存储fn的函数地址,保障永远不变
  const memoizedFn = useRef<PickFunction<T>>();
  if (!memoizedFn.current) {
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args);
    };
  }

  return memoizedFn.current as T;
}

export default useMemoizedFn;

useReactive

提供一种数据响应式的操作体验,定义数据状态不需要写useState,直接修改属性即可刷新视图(vue体验感)。

用法

javascript 复制代码
const state = useReactive({
    count: 0,
    inputVal: '',
    obj: {
      value: '',
    },
  });

  return (
    <div>
      <p> state.count:{state.count}</p>

      <button style={{ marginRight: 8 }} onClick={() => state.count++}>
    </div>
  )

实现逻辑:通过代理value的修改,隐性插入useRef记录数据,在每次数据变更的时候都强制进行组件刷新

  • proxy:用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
  • Reflect:提供拦截 JavaScript 操作的方法
typescript 复制代码
// k:v 原对象:代理过的对象
const proxyMap = new WeakMap();
// k:v 代理过的对象:原对象
const rawMap = new WeakMap();

function useReactive<S extends Record<string, any>>(initialState: S): S {
  // 初始化时刷新组件
  const update = useUpdate();
  const stateRef = useRef<S>(initialState);

  const state = useCreation(() => {
    return observer(stateRef.current, () => {
      update();
    });
  }, []);

// 通过proxy代理每次value修改,触发cb(update)
function observer<T extends Record<string, any>>(initialVal: T, cb: () => void): T {
  const existingProxy = proxyMap.get(initialVal);

  // 添加缓存 防止重新构建proxy
  if (existingProxy) {
    return existingProxy;
  }

  // 防止代理已经代理过的对象
  // https://github.com/alibaba/hooks/issues/839
  if (rawMap.has(initialVal)) {
    return initialVal;
  }

  const proxy = new Proxy<T>(initialVal, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);

      // https://github.com/alibaba/hooks/issues/1317
      const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
      if (!descriptor?.configurable && !descriptor?.writable) {
        return res;
      }
      // Only proxy plain object or array,
      // otherwise it will cause: https://github.com/alibaba/hooks/issues/2080
      return isPlainObject(res) || Array.isArray(res) ? observer(res, cb) : res;
    },
    set(target, key, val) {
      const ret = Reflect.set(target, key, val);
      cb();
      return ret;
    },
    deleteProperty(target, key) {
      const ret = Reflect.deleteProperty(target, key);
      cb();
      return ret;
    },
  });

  proxyMap.set(initialVal, proxy);
  rawMap.set(proxy, initialVal);

  return proxy;
}
  return state;
}

Dev Hooks

在开发环境下执行的hooks

useTrackedEffect

追踪是哪个依赖变化触发了 useEffect 的执行。

原理:用useRef记录每次修改后的dep,与下次dep进行diff判断哪些依赖变更了

ini 复制代码
const diffTwoDeps = (deps1?: DependencyList, deps2?: DependencyList) => {
  return deps1
    ? deps1
        .map((_ele, idx) => (!Object.is(deps1[idx], deps2?.[idx]) ? idx : -1))
        .filter((ele) => ele >= 0)
    : deps2
    ? deps2.map((_ele, idx) => idx)
    : [];
};

const useTrackedEffect = <T extends DependencyList>(effect: Effect<T>, deps?: [...T]) => {
  // 记录每次修改后的dep,与下次dep进行diff判断哪些依赖变更了
  const previousDepsRef = useRef<T>();

  useEffect(() => {
    const changes = diffTwoDeps(previousDepsRef.current, deps);
    const previousDeps = previousDepsRef.current;
    previousDepsRef.current = deps;
    return effect(changes, previousDeps, deps);
  }, deps);
};

useWhyDidYouUpdate

帮助开发者排查是哪个属性改变导致了组件的 rerender

原理和useTrackedEffect一样

ini 复制代码
export default function useWhyDidYouUpdate(componentName: string, props: IProps) {
  // 记录props
  const prevProps = useRef<IProps>({});

  useEffect(() => {
    if (prevProps.current) {
      const allKeys = Object.keys({ ...prevProps.current, ...props });
      const changedProps: IProps = {};

      allKeys.forEach((key) => {
        if (!Object.is(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;
  });
} 

useRequest🌟

useRequest 是一个强大的异步数据管理的 Hooks,通过插件式组织代码,其核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:

  • 自动请求/手动请求
  • 轮询
  • 防抖
  • 节流
  • 屏幕聚焦重新请求
  • 错误重试
  • loading delay
  • SWR(stale-while-revalidate)
  • 缓存

我们逐步解析这些能力的源码实现

自动请求/手动请求

用例

arduino 复制代码
// 自动请求
const { data, error, loading } = useRequest(getUsername);
// 手动请求
const { loading, run } = useRequest(changeUsername, {
  manual: true
});

源码分析

scss 复制代码
function useRequest<TData, TParams extends any[]>(
  service: Service<TData, TParams>, // 需要执行的函数
  options?: Options<TData, TParams>, // 可配置参数
  plugins?: Plugin<TData, TParams>[], // 插件
) { 
  return useRequestImplement<TData, TParams>(
      service, 
      options, 
      [
        ...(plugins || []), 
        useDebouncePlugin,
        useLoadingDelayPlugin,
        usePollingPlugin,
        useRefreshOnWindowFocusPlugin,
        useThrottlePlugin,
        useAutoRunPlugin,
        useCachePlugin,
        useRetryPlugin,
      ] as Plugin<TData, TParams>[]);
}

// useRequestImplement
function useRequestImplement<TData, TParams extends any[]>(
  service: Service<TData, TParams>,
  options: Options<TData, TParams> = {},
  plugins: Plugin<TData, TParams>[] = [],
) {
  const { manual = false, ...rest } = options;
  // options
  const fetchOptions = {
    manual,
    ...rest,
  };
  // 获取最新的service fn
  const serviceRef = useLatest(service);
  // 首次需要强制刷新,初始化默认值
  const update = useUpdate();
  // 利用useCreation创建fetch 实例,仅在第一次更新
  const fetchInstance = useCreation(() => {
    // 执行插件
    const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
    // 发起请求
    return new Fetch<TData, TParams>(
      serviceRef,
      fetchOptions,
      update,
      Object.assign({}, ...initState),
    );
  }, []);
  fetchInstance.options = fetchOptions;
  // run all plugins hooks
  fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));

  useMount(() => {
    if (!manual) {
      // 不是手动触发,首次自动触发
      // useCachePlugin can set fetchInstance.state.params from cache when init
      const params = fetchInstance.state.params || options.defaultParams || [];
      // @ts-ignore
      fetchInstance.run(...params);
    }
  });

  useUnmount(() => {
    // 取消请求
    fetchInstance.cancel();
  });
  // 返回fetch状态
  return {
    loading: fetchInstance.state.loading,
    data: fetchInstance.state.data,
    error: fetchInstance.state.error,
    params: fetchInstance.state.params || [],
    cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)),
    refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)),
    refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)),
    run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)),
    runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)),
    mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)),
  } as Result<TData, TParams>;
}

从这可以了解到Fetch作为useRequest的核心承接了请求的发起、状态管理、取消等能力

Fetch

kotlin 复制代码
// packages/hooks/src/useRequest/src/Fetch.ts
export default class Fetch<TData, TParams extends any[]> {
  pluginImpls: PluginReturn<TData, TParams>[];
  // 记录请求是否正常执行完成
  count: number = 0;
  state: FetchState<TData, TParams> = {
    loading: false,
    params: undefined,
    data: undefined,
    error: undefined,
  };

  constructor(
    public serviceRef: MutableRefObject<Service<TData, TParams>>,
    public options: Options<TData, TParams>,
    public subscribe: Subscribe,
    public initState: Partial<FetchState<TData, TParams>> = {},
  ) {
  // 初始化
    this.state = {
      ...this.state,
      loading: !options.manual,
      ...initState,
    };
  }
  // 更新state
  setState(s: Partial<FetchState<TData, TParams>> = {}) {
    this.state = {
      ...this.state,
      ...s,
    };
    // update(),强制刷新组件
    this.subscribe();
  }
  // 执行插件(实现轮询、ready、防抖等)
  runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
    // @ts-ignore
    const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
    return Object.assign({}, ...r);
  }
  // 处理异步run函数
  async runAsync(...params: TParams): Promise<TData> {
    this.count += 1;
    const currentCount = this.count;

    const {
      stopNow = false,
      returnNow = false,
      ...state
    } = this.runPluginHandler('onBefore', params);

    // stop request
    if (stopNow) {
      return new Promise(() => {});
    }

    this.setState({
      loading: true,
      params,
      ...state,
    });

    // return now
    if (returnNow) {
      return Promise.resolve(state.data);
    }

    this.options.onBefore?.(params);

    try {
      // replace service
      let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);

      if (!servicePromise) {
        servicePromise = this.serviceRef.current(...params);
      }
      // 执行service
      const res = await servicePromise;
      if (currentCount !== this.count) {
        // prevent run.then when request is canceled
        return new Promise(() => {});
      }

      this.setState({
        data: res,
        error: undefined,
        loading: false,
      });
      // 执行对于的hooks cb
      this.options.onSuccess?.(res, params);
      this.runPluginHandler('onSuccess', res, params);

      this.options.onFinally?.(params, res, undefined);

      if (currentCount === this.count) {
        this.runPluginHandler('onFinally', params, res, undefined);
      }

      return res;
    } catch (error) {
      if (currentCount !== this.count) {
        // prevent run.then when request is canceled
        return new Promise(() => {});
      }

      this.setState({
        error,
        loading: false,
      });

      this.options.onError?.(error, params);
      this.runPluginHandler('onError', error, params);

      this.options.onFinally?.(params, undefined, error);

      if (currentCount === this.count) {
        this.runPluginHandler('onFinally', params, undefined, error);
      }

      throw error;
    }
  }
  // 暴露run函数
  run(...params: TParams) {
    this.runAsync(...params).catch((error) => {
      if (!this.options.onError) {
        console.error(error);
      }
    });
  }

  cancel() {
    this.count += 1;
    this.setState({
      loading: false,
    });

    this.runPluginHandler('onCancel');
  }

  refresh() {
    // @ts-ignore
    this.run(...(this.state.params || []));
  }

  refreshAsync() {
    // @ts-ignore
    return this.runAsync(...(this.state.params || []));
  }
  
  mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
    const targetData = isFunction(data) ? data(this.state.data) : data;
    this.runPluginHandler('onMutate', targetData);
    this.setState({
      data: targetData,
    });
  }
}

到这儿整个逻辑都清晰啦

  1. useRequestImplement

    初始化fetch(注入参数和插件),并处理mount和unmount

  2. Fetch 收集请求整个过程的state,并在每次state更新的时候都通过useUpdate()强制刷新组件更新页面

    1. 在对应阶段(sucessed or failed)触发plugins hooks
  3. 通过插件提供其他的能力,包括轮询、Ready等

接着我们继续看一些关键的能力

轮询(usePollingPlugin)

通过设置 options.pollingInterval,进入轮询模式,useRequest 会定时触发 service 执行。

通过在请求onfinish的时候setTimeout延时执行(不断递归调用),并在外层记录轮询次数和时间

ini 复制代码
const { data, run, cancel } = useRequest(getUsername, {
  pollingInterval: 3000,
});
// usePollingPlugin
const usePollingPlugin: Plugin<any, any[]> = (
  fetchInstance,
  { pollingInterval, pollingWhenHidden = true, pollingErrorRetryCount = -1 },
) => {
  // 记录时间
  const timerRef = useRef<Timeout>();
  const unsubscribeRef = useRef<() => void>();
  // 记录轮询次数
  const countRef = useRef<number>(0);
  // 停止轮询
  const stopPolling = () => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }
    unsubscribeRef.current?.();
  };
  // 到时间停止轮询
  useUpdateEffect(() => {
    if (!pollingInterval) {
      stopPolling();
    }
  }, [pollingInterval]);
  
  if (!pollingInterval) {
    return {};
  }

  return {
    onBefore: () => {
      stopPolling();
    },
    // 记录轮询次数
    onError: () => {
      countRef.current += 1;
    },
    onSuccess: () => {
      countRef.current = 0;
    },
    onFinally: () => {
      if (
        pollingErrorRetryCount === -1 ||
        // 当发生错误时,在轮询错误重试次数后,请求不会重复
        (pollingErrorRetryCount !== -1 && countRef.current <= pollingErrorRetryCount)
      ) {
        timerRef.current = setTimeout(() => {
          // 通过setTimeout进行轮询,
          if (!pollingWhenHidden && !isDocumentVisible()) {
            unsubscribeRef.current = subscribeReVisible(() => {
              fetchInstance.refresh();
            });
          } else {
            fetchInstance.refresh();
          }
        }, pollingInterval);
      } else {
        countRef.current = 0;
      }
    },
    onCancel: () => {
      stopPolling();
    },
  };
};

防抖(useDebouncePlugin)

通过debounce封装runAsync后注入回fetchInstance

ini 复制代码
const useDebouncePlugin: Plugin<any, any[]> = (
  fetchInstance,
  { debounceWait, debounceLeading, debounceTrailing, debounceMaxWait },
) => {
  // 记录 debounced 封装的函数
  const debouncedRef = useRef<DebouncedFunc<any>>();
  // init options
  const options = useMemo(() => {
    const ret: DebounceSettings = {};
    if (debounceLeading !== undefined) {
      ret.leading = debounceLeading;
    }
    if (debounceTrailing !== undefined) {
      ret.trailing = debounceTrailing;
    }
    if (debounceMaxWait !== undefined) {
      ret.maxWait = debounceMaxWait;
    }
    return ret;
  }, [debounceLeading, debounceTrailing, debounceMaxWait]);

  useEffect(() => {
    if (debounceWait) {
      // 从fetchInstance.runAsync创建 runAsync 函数
      const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
      // 创建基于options防抖执行的函数
      debouncedRef.current = debounce(
        (callback) => {
          callback();
        },
        debounceWait,
        options,
      );
      
      // debounce runAsync should be promise
      // https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
      // 使用 debounce 与返回 Promise 的函数一起使用,在第一次调用时不会返回 Promise。
      // 所以使用promise再包一层
      fetchInstance.runAsync = (...args) => {
        return new Promise((resolve, reject) => {
          debouncedRef.current?.(() => {
            _originRunAsync(...args)
              .then(resolve)
              .catch(reject);
          });
        });
      };

      return () => {
        debouncedRef.current?.cancel();
        // 修改fetchInstance.runAsync,保障其他的plugin执行run的时候仍是debounce处理过的
        fetchInstance.runAsync = _originRunAsync;
      };
    }
  }, [debounceWait, options]);

  if (!debounceWait) {
    return {};
  }

  return {
    onCancel: () => {
      debouncedRef.current?.cancel();
    },
  };
};

由此可见plugin的执行顺序也是至关重要的

节流

实现逻辑和防抖一样,就是将debounce改为了throttle。都来自于 lodash

缓存 & SWR

"SWR" 这个名字来自于 stale-while-revalidate:一种由 HTTP RFC 5861(opens in a new tab) 推广的 HTTP 缓存失效策略。这种策略首先从缓存中返回数据(过期的),同时发送 fetch 请求(重新验证),最后得到最新数据。

  1. 通过cache存储缓存的数据

    1. 若有缓存数据,不继续请求,使用缓存数据
  2. 基于发布订阅者设计,在下次组件初始化时,如果有缓存数据,通知fetchInstance 初始化state返回缓存数据,再发起请求。实现SWR

    1. 本质用一个数组把需要SWR的fetchInstance 都存起来了
    2. 然后在每次组件重渲染的时候遍历这个数组,从cache里读出数据设置每个fetchInstance的state

无论是cache还是subscribed都是全局变量,里面存储的整个页面的fetch数据

ini 复制代码
// 存储需要缓存的数据
const cache = new Map<CachedKey, RecordData>();
const setCache = (key: CachedKey, cacheTime: number, cachedData: CachedData) => {
  const currentCache = cache.get(key);
  if (currentCache?.timer) {
    clearTimeout(currentCache.timer);
  }
  let timer: Timer | undefined = undefined;
  if (cacheTime > -1) {
    // if cache out, clear it
    timer = setTimeout(() => {
      cache.delete(key);
    }, cacheTime);
  }
  cache.set(key, {
    ...cachedData,
    timer,
  });
};

const getCache = (key: CachedKey) => {
  return cache.get(key);
};

const clearCache = (key?: string | string[]) => {
  if (key) {
    const cacheKeys = Array.isArray(key) ? key : [key];
    cacheKeys.forEach((cacheKey) => cache.delete(cacheKey));
  } else {
    cache.clear();
  }
};

const useCachePlugin: Plugin<any, any[]> = (
  fetchInstance,
  {
    cacheKey,
    cacheTime = 5 * 60 * 1000,
    staleTime = 0,
    setCache: customSetCache,
    getCache: customGetCache,
  },
) => {
  const unSubscribeRef = useRef<() => void>();

  const currentPromiseRef = useRef<Promise<any>>();
  // 自定义缓存比如存储到localStorage
  const _setCache = (key: string, cachedData: CachedData) => {
    if (customSetCache) {
      customSetCache(cachedData);
    } else {
      setCache(key, cacheTime, cachedData);
    }
    // 发布订阅者 触发每一个订阅者 更新fetchInstance的state
    trigger(key, cachedData.data);
  };

  const _getCache = (key: string, params: any[] = []) => {
    if (customGetCache) {
      return customGetCache(params);
    }
    return getCache(key);
  };
  // 仅第一次自动执行
  useCreation(() => {
    if (!cacheKey) {
      return;
    }
    // get data from cache when init
    const cacheData = _getCache(cacheKey);
    if (cacheData && Object.hasOwnProperty.call(cacheData, 'data')) {
      fetchInstance.state.data = cacheData.data;
      fetchInstance.state.params = cacheData.params;
      if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
        fetchInstance.state.loading = false;
      }
    }
    // 通知fetchInstance 初始化state (swr)
    // subscribe same cachekey update, trigger update
    unSubscribeRef.current = subscribe(cacheKey, (data) => {
      fetchInstance.setState({ data });
    });
  }, []);

  useUnmount(() => {
    unSubscribeRef.current?.();
  });

  if (!cacheKey) {
    return {};
  }

  return {
    onBefore: (params) => {
      const cacheData = _getCache(cacheKey, params);
      if (!cacheData || !Object.hasOwnProperty.call(cacheData, 'data')) {
        return {};
      }
      // 若有缓存数据,不继续请求,使用缓存数据
      if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
        return {
          loading: false,
          data: cacheData?.data,
          error: undefined,
          returnNow: true,
        };
      } else {
        // If the data is stale, return data, and request continue
        return {
          data: cacheData?.data,
          error: undefined,
        };
      }
    },
    onRequest: (service, args) => {
      let servicePromise = getCachePromise(cacheKey);
      // 如果有servicePromise,并且不是由自身触发,则使用它
      if (servicePromise && servicePromise !== currentPromiseRef.current) {
        return { servicePromise };
      }
      servicePromise = service(...args);
      currentPromiseRef.current = servicePromise;
      setCachePromise(cacheKey, servicePromise);
      return { servicePromise };
    },
    onSuccess: (data, params) => {
      if (cacheKey) {
        // cancel subscribe, avoid trgger self
        // 订阅者出对列,清除需要设置state的fetchInstance
        unSubscribeRef.current?.();
        // 设置缓存值
        _setCache(cacheKey, {
          data,
          params,
          time: new Date().getTime(),
        });
        // resubscribe
        // 订阅者入对列
        unSubscribeRef.current = subscribe(cacheKey, (d) => {
          fetchInstance.setState({ data: d });
        });
      }
    },
    // 直接修改 data的时候也更新缓存
    onMutate: (data) => {
      if (cacheKey) {
        // cancel subscribe, avoid trigger self
        unSubscribeRef.current?.();
        _setCache(cacheKey, {
          data,
          params: fetchInstance.state.params,
          time: new Date().getTimo xme(),
        });
        // resubscribe
        unSubscribeRef.current = subscribe(cacheKey, (d) => {
          fetchInstance.setState({ data: d });
        });
      }
    },
  };
};

总结

以上我们把ahooks的hook全部都过了一遍,并且是一行行代码看,增进了在日常开发中对react的使用理解,对于不同的渲染场景我们也知道了该使用什么方法来解决,但也别搭建过多的hook导致代码阅读变复杂~~

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax