React中如何实现元素跟随鼠标的平滑缓动滑动效果?
实现图片鼠标跟随的平滑滑动效果
问题背景
现有React组件实现了两个带图片的面板,支持滚轮缩放,缩放后图片会跟随鼠标移动。但尝试用CSS transition实现平滑跟随效果时,画面卡顿,只有鼠标停止时变换才会更新,需要实现带有轻微滞后或动态速度的平滑滑动效果。
解决方案一:用requestAnimationFrame做渐进式过渡
核心思路是拆分目标状态和当前过渡状态,通过逐帧计算的方式让当前位置逼近目标位置,避免React状态频繁更新导致的卡顿。
修改后的完整代码:
import React, { useState, useRef, useEffect } from 'react' import './App.css' import PaintingPanel, {PaintingPanelProps, PaintingPosition} from './PaintingPanel'; const App: React.FC = () => { // 存储最终目标位置,仅在鼠标/滚轮操作时更新 const [targetPosition, setTargetPosition] = useState<PaintingPosition>({ zoomRatio: 1, xFraction: 0, yFraction: 0 }) // 存储当前过渡中的位置,不触发组件重渲染 const currentPosition = useRef<PaintingPosition>({...targetPosition}) const animationFrameRef = useRef<number>() const paintingName = 'test'; const zoomPainting = (e: WheelEvent) => { const minZoomRatio = 1.0; const maxZoomRatio = 2.4; const newTargetPosition = {...targetPosition} const nextZoomRatio = newTargetPosition.zoomRatio + -e.deltaY/1000; newTargetPosition.zoomRatio = Math.min(Math.max(nextZoomRatio, minZoomRatio), maxZoomRatio) setTargetPosition(newTargetPosition) } function createFollowMouseOnPainting(panelId: string) { return (e: MouseEvent) => { const paintingPanel: HTMLDivElement = document.getElementById(panelId); const rect = paintingPanel.getBoundingClientRect(); const targetWidth = paintingPanel.clientWidth; const targetHeight = paintingPanel.clientHeight; setTargetPosition(prev => ({ ...prev, xFraction: (e.clientX - rect.left)/targetWidth, yFraction: (e.clientY - rect.top)/targetHeight })) } } // 逐帧更新过渡位置,逼近目标值 useEffect(() => { const smoothTransition = () => { // 平滑系数:值越小,滞后感越强,平滑效果越明显(建议0.1~0.2) const smoothFactor = 0.15; currentPosition.current.xFraction += (targetPosition.xFraction - currentPosition.current.xFraction) * smoothFactor; currentPosition.current.yFraction += (targetPosition.yFraction - currentPosition.current.yFraction) * smoothFactor; currentPosition.current.zoomRatio += (targetPosition.zoomRatio - currentPosition.current.zoomRatio) * smoothFactor; animationFrameRef.current = requestAnimationFrame(smoothTransition); } animationFrameRef.current = requestAnimationFrame(smoothTransition); // 组件卸载时取消动画帧 return () => { animationFrameRef.current && cancelAnimationFrame(animationFrameRef.current); } }, [targetPosition]) // 根据当前过渡位置计算变换值 const xTransform = 100*((0.5 - currentPosition.current.xFraction) * (1-(1/currentPosition.current.zoomRatio))); const yTransform = 100*((0.5 - currentPosition.current.yFraction) * (1-(1/currentPosition.current.zoomRatio))); const paintingImgStyle: React.CSSProperties = { transform: `scale(${currentPosition.current.zoomRatio}) translateX(${xTransform}%) translateY(${yTransform}%)`, willChange: 'transform', // 提示浏览器提前优化动画 } const panelOneProps: PaintingPanelProps = { paintingName: paintingName, isDiff: false, paintingImgStyle, zoomPainting, createFollowMouseOnPainting } const panelTwoProps: PaintingPanelProps = { paintingName: paintingName, isDiff: true, paintingImgStyle, zoomPainting, createFollowMouseOnPainting } return ( <div id='panel-container'> <PaintingPanel {...panelOneProps} /> <PaintingPanel {...panelTwoProps} /> </div> ) } export default App
关键细节
- 用
targetPosition维护最终目标状态,仅在用户操作时更新,减少重渲染频率 currentPosition通过useRef存储,更新时不会触发组件重渲染,避免React状态更新带来的额外开销- 调整
smoothFactor可以自定义平滑程度,数值越大跟随速度越快,越小则滞后感越强
解决方案二:优化CSS Transition(针对原卡顿问题修复)
如果想保留CSS过渡的实现方式,需要解决状态更新不连续的问题:
- 用
useMemo缓存样式,避免不必要的重计算 - 使用函数式更新获取最新状态,保证过渡连续性
- 明确指定过渡属性和时长
修改后的关键代码片段:
import { useMemo } from 'react' // ...其他代码不变 // 用useMemo缓存样式,仅依赖变化的参数 const paintingImgStyle: React.CSSProperties = useMemo(() => ({ transform: `scale(${paintingPosition.zoomRatio}) translateX(${xTransform}%) translateY(${yTransform}%)`, transition: 'transform 0.1s ease-out', // 指定过渡属性和时长 willChange: 'transform', }), [xTransform, yTransform, paintingPosition.zoomRatio]) // 鼠标事件处理改为函数式更新 function createFollowMouseOnPainting(panelId: string) { return (e: MouseEvent) => { const paintingPanel: HTMLDivElement = document.getElementById(panelId); const rect = paintingPanel.getBoundingClientRect(); const targetWidth = paintingPanel.clientWidth; const targetHeight = paintingPanel.clientHeight; const xFraction = (e.clientX - rect.left)/targetWidth; const yFraction = (e.clientY - rect.top)/targetHeight; setPaintingPosition(prev => ({ ...prev, xFraction, yFraction })) } }
说明
这种方式实现简单,但平滑效果的可控性不如requestAnimationFrame方案,如果还是存在卡顿,优先选择第一种方案。
内容的提问来源于stack exchange,提问作者Rory McDonald
相关产品推荐
相关产品推荐

