偷师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导致代码阅读变复杂~~

相关推荐
程序员爱技术1 小时前
Vue 2 + JavaScript + vue-count-to 集成案例
前端·javascript·vue.js
并不会2 小时前
常见 CSS 选择器用法
前端·css·学习·html·前端开发·css选择器
衣乌安、2 小时前
【CSS】居中样式
前端·css·css3
兔老大的胡萝卜2 小时前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
低代码布道师2 小时前
CSS的三个重点
前端·css
耶啵奶膘3 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^5 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie5 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic6 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿6 小时前
webWorker基本用法
前端·javascript·vue.js