React Hooks useReducer 逃避deps组件渲染次数增加陷阱

前言

在快乐使用 React Hooks 开发自定义 Hooks 过程中,使用了 useEffectuseReduceruseRefuseCallback 等官方提供的 Hooks,将一些通用逻辑抽离出来,提高代码复用性。

但在组合使用 useEffectuseReducerReact.memo 时,发生了组件在状态未发生变化时触发渲染,因为此动作发生在 mousemove 鼠标移动时,所以组件不必要渲染次数非常多。

自定义 Hooks 简单实现

import { useReducer } from "react";
const reducer = (state, action) => {
  const { sliding, lastPos, ratio } = state;
  switch (action.type) {
    case "start":
      return {
        ...state,
        slideRange: action.slideRange,
        lastPos: action.x,
        sliding: true,
      };
    case "move":
      if (!sliding) {
        return state;
      }
      const offsetX = action.x - lastPos;
      const newRatio = ratio + offsetX / state.slideRange;
      if (newRatio > 1 || newRatio < 0) {
        return state;
      }
      return {
        ...state,
        lastPos: action.x,
        ratio: newRatio,
      };
    case "end":
      if (!sliding) {
        return state;
      }
      return {
        ...state,
        sliding: false,
      };
    case "updateRatio":
      return {
        ...state,
        ratio: action.ratio,
      };
    default:
      return state;
  }
};
export function useSlider(initialState) {
    const [state, dispatch] = useReducer(reducer, initialState);
    return [state, dispatch];
}

在组件中使用自定义 Hooks

const [state, dispatch] = useSlider(initialState);
  const { ratio, sliding, lastPos, slideRange } = state;
  useEffect(() => {
    const onSliding = (e) => {
      dispatch({ type: "move", x: e.pageX });
    };
    const onSlideEnd = () => {
      dispatch({ type: "end" });
    };
    document.addEventListener("mousemove", onSliding);
    document.addEventListener("mouseup", onSlideEnd);
    return () => {
      document.removeEventListener("mousemove", onSliding);
      document.removeEventListener("mouseup", onSlideEnd);
    };
  }, [dispatch]);
  const handleThumbMouseDown = useCallback(
    (event) => {
      const hotArea = hotAreaRef.current;
      dispatch({
        type: "start",
        x: event.pageX,
        slideRange: hotArea.clientWidth,
      });
      if (event.target.className !== "point") {
        dispatch({
          type: "updateRatio",
          ratio: (event.pageX - 30) / hotArea.clientWidth,
        });
      }
    },
    [dispatch]
  );

鼠标每次移动,都会触发 dispatch({ type: "move", x: e.pageX }),在 reducer 函数中,当 !sliding 时,不修改 state 数据原样返回,但是组件仍然进行了渲染。

提前阻止 dispatch 触发

sliding 判断移动到 useEffect 中,提前阻止 dispatch 触发,并将 sliding 设置到 useEffect(fn, deps) deps 中,保证监听函数中能取到 sliding 最新值。

useEffect(() => {
    const onSliding = (e) => {
      if(!sliding) {
        return;
      }
      dispatch({ type: "move", x: e.pageX });
    };
    const onSlideEnd = () => {
      if (!sliding) {
        return;
      }
      dispatch({ type: "end" });
    };
    document.addEventListener("mousemove", onSliding);
    document.addEventListener("mouseup", onSlideEnd);
    return () => {
      document.removeEventListener("mousemove", onSliding);
      document.removeEventListener("mouseup", onSlideEnd);
    };
  }, [sliding]);

优化后再测试

鼠标仅移动时,slidingfalse,直接 return,不会触发 dispatch 动作。

好处

避免了组件在 state 未修改时不必要渲染。

坏处

部分处理逻辑被移动到使用自定义 hooks 的组件中,sliding 数据改变时,add EventListener函数会重新注册。

结论

不能为了不在 useEffect(fn, deps) 设置 deps,使用 useReducer,并把所有数据变更都放在 reducer 中。 本篇文章通过把不修改 reducer state 的动作提前阻止,避免使用此自定义 hooks 的组件发生不必要渲染,提高代码复用性的同时也兼顾了组件性能。

题外

  • React.memoprops 进行浅比较,一种组件性能优化方式
  • useCallback(fn, deps) 缓存函数
  • useMemo(() => fn, deps) 缓存昂贵变量
  • useState(initialState)惰性初始stateinitialState` 只会在组件初始渲染中起作用,后续渲染时会被

参考文献

React 官方文档

以上就是React Hooks 之 useReducer 逃避deps后增加组件渲染次数的陷阱的详细内容,更多关于React Hooks useReducer组件渲染的资料请关注脚本之家其它相关文章!

你可能感兴趣的:(React Hooks useReducer 逃避deps组件渲染次数增加陷阱)