React自定义useDrag Hook触发mousemove事件导致页面渲染卡顿如何优化?
现有实现的核心问题
- 你使用
useState存储dragStart和dragEnd,每次mousemove触发setDragEnd都会强制调用useDrag的父组件全量重渲染,mousemove每秒触发60+次,相当于每秒触发60+次组件重渲染,自然会出现卡顿甚至冻结 - useEffect依赖项包含
dragStart,每次拖拽开始/结束都会重新绑定一遍事件监听器,产生冗余操作 - 拖拽选框的样式更新走React状态渲染链路,性能开销远高于直接操作DOM
可行优化方案
- 拖拽过程中的临时坐标改用
useRef存储,更新ref不会触发组件重渲染,仅在拖拽结束需要提交结果时再更新状态 - 用
requestAnimationFrame控制选框样式更新频率,和浏览器刷新率对齐,避免无效的高频更新 - 事件监听器绑定到document对象上,避免鼠标移出绑定区域后拖拽中断,同时优化useEffect依赖项,减少重复绑定
- 若确实需要在父组件获取实时拖拽坐标,可添加节流逻辑,控制状态更新频率不超过60次/秒
- 如果仅需要渲染拖拽选框,可将选框DOM操作封装在useDrag内部,完全绕开React状态渲染链路,性能最优
优化后代码示例
import { useRef, useState, useEffect } from 'react'; // 先定义Coordinates类型 type Coordinates = { x: number; y: number } | null; // 辅助函数 获取鼠标相对于元素的偏移 const getMouseOffset = (e: MouseEvent, element: HTMLElement | null): Coordinates => { if (!element) return null; const rect = element.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top, }; }; export const useDrag = (selectionRef: React.MutableRefObject<HTMLDivElement | null>) => { // 临时坐标存在ref里 不触发重渲染 const dragStartRef = useRef<Coordinates>(null); const dragEndRef = useRef<Coordinates>(null); // 只有需要对外暴露的状态才用useState 可以按需控制更新频率 const [pendingSelection, setPendingSelection] = useState<{ start: Coordinates; end: Coordinates } | null>(null); // raf id用来取消帧回调 const rafIdRef = useRef<number | null>(null); const onMouseDown = (e: MouseEvent) => { dragStartRef.current = getMouseOffset(e, selectionRef.current); }; const onMouseUp = () => { dragStartRef.current = null; dragEndRef.current = null; setPendingSelection(null); if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current); }; const onMouseMove = (e: MouseEvent) => { if (!dragStartRef.current) return; dragEndRef.current = getMouseOffset(e, selectionRef.current); // 用raf控制更新频率 对齐浏览器刷新率 if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current); rafIdRef.current = requestAnimationFrame(() => { if (dragStartRef.current && dragEndRef.current) { setPendingSelection({ start: dragStartRef.current, end: dragEndRef.current, }); } }); }; useEffect(() => { // 事件绑定到document 避免拖拽移出元素失效 document.addEventListener('mousedown', onMouseDown); document.addEventListener('mouseup', onMouseUp); document.addEventListener('mousemove', onMouseMove); return () => { document.removeEventListener('mousedown', onMouseDown); document.removeEventListener('mouseup', onMouseUp); document.removeEventListener('mousemove', onMouseMove); if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current); }; }, []); // 依赖项为空 只绑定一次 return { pendingSelection }; };
如果性能要求极高,不需要在父组件使用pendingSelection做其他逻辑,仅用于渲染选框,可以进一步改造为直接操作DOM,完全不需要状态触发重渲染:
// 调用侧代码 const selectionRef = useRef<HTMLDivElement>(null); const selectBoxRef = useRef<HTMLDivElement>(null); // 改造useDrag传入selectBoxRef,内部直接修改style.left/top/width/height属性即可 return ( <div className="selection" ref={selectionRef} style={{ position: 'relative' }}> <div ref={selectBoxRef} style={{ position: 'absolute', background: 'rgba(0,0,0,0.1)', pointerEvents: 'none' }}></div> </div> );
内容的提问来源于stack exchange,提问作者Michael Sanchez
相关产品推荐
相关产品推荐

