目录
- 前言
- 自定义 Hooks 简单实现
- 在组件中使用自定义 Hooks
- 提前阻止 dispatch 触发
- 结论
- 题外
前言
在快乐使用 React Hooks 开发自定义 Hooks 过程中,使用了 useEffect,useReducer,useRef,useCallback 等官方提供的 Hooks,将一些通用逻辑抽离出来,提高代码复用性。
但在组合使用 useEffect,useReducer,React.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]);
优化后再测试
鼠标仅移动时,sliding 为 false,直接 return,不会触发 dispatch 动作。
好处
避免了组件在 state 未修改时不必要渲染。
坏处
部分处理逻辑被移动到使用自定义 hooks 的组件中,sliding 数据改变时,add EventListener函数会重新注册。
结论
不能为了不在 useEffect(fn, deps) 设置 deps,使用 useReducer,并把所有数据变更都放在 reducer 中。 本篇文章通过把不修改 reducer state 的动作提前阻止,避免使用此自定义 hooks 的组件发生不必要渲染,提高代码复用性的同时也兼顾了组件性能。
题外
-
React.memo 对 props 进行浅比较,一种组件性能优化方式
-
useCallback(fn, deps) 缓存函数
-
useMemo(() => fn, deps) 缓存昂贵变量
- useState(initialState)惰性初始state
,initialState` 只会在组件初始渲染中起作用,后续渲染时会被
参考文献
React 官方文档