You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React onWheel/onScroll 实现缩放滚动动画的最佳实践及卡顿修复

问题根因分析

  • 异步状态闭包滞后:你当前代码中读取的scale、scrollTop都属于当前渲染闭包的旧值,React useState 是异步批量更新的,快速滚动时连续触发的wheel事件会复用同一个旧状态计算新的缩放值,导致更新逻辑失效,出现卡住的现象。
  • 未阻止默认滚轮行为:你在处理缩放逻辑时仅返回null,没有调用e.preventDefault()阻止原生滚动行为,浏览器对快速连续的wheel事件有内置的节流优化,当浏览器识别到原生滚动正在执行时,会暂时暂停JS侧的事件派发,直到节流周期结束,这就是你不移动鼠标就卡住的核心原因。另外React 17+ 版本的合成事件默认将wheel事件设置为passive: true,直接在合成事件中调用preventDefault会失效,需要额外处理。
  • 重复className属性:你的App根元素写了两次className属性,会导致前面绑定事件的className被后面的覆盖,overflow的锁定设置不生效。
  • 固定步长缩放和滚轮滚动幅度不匹配,无论用户滚轮滚动的快慢,每次都固定增减0.05,不符合用户操作直觉,也容易出现缩放节奏异常。

修复方案

第一步:调整状态管理,用useRef存储最新状态避免闭包问题

const [scrolledOut, setScrolledOut] = React.useState(false)
const [scrollTop, setScrollTop] = React.useState(0)
const [scale, setScale] = React.useState(1)
// 新增ref存储最新状态,避免wheel事件读旧值
const scaleRef = React.useRef(1)
const scrollTopRef = React.useRef(0)
const scrolledOutRef = React.useRef(false)

// 同步state到ref
React.useEffect(() => {
  scaleRef.current = scale
}, [scale])
React.useEffect(() => {
  scrollTopRef.current = scrollTop
}, [scrollTop])
React.useEffect(() => {
  scrolledOutRef.current = scrolledOut
}, [scrolledOut])

const { height, width } = useWindowDimensions();

function checkLocking(n) {
  const newScrolledOut = n < 0.36 || scrollTopRef.current > 0
  setScrolledOut(newScrolledOut)
}

第二步:修改wheel事件逻辑,阻止默认行为,用动态计算缩放步长

使用原生事件绑定来关闭passive模式:

const appRef = React.useRef(null)

React.useEffect(() => {
  if(width <= 991 || !appRef.current) return
  const handleWheel = (e) => {
    const currentScale = scaleRef.current
    const currentScrollTop = scrollTopRef.current
    const scrollAmount = e.deltaY
    // 处理向下滚动缩放
    if(scrollAmount > 0 && currentScale > 0.35 && currentScrollTop === 0) {
      e.preventDefault()
      // 根据滚轮delta值计算缩放步长,避免固定步长
      const delta = Math.min(scrollAmount / 100, 0.1)
      const newScale = Math.max(0.35, currentScale - delta)
      setScale(parseFloat(newScale.toFixed(2)))
      checkLocking(newScale)
      return
    } 
    // 处理向上滚动放大
    if(scrollAmount < 0 && currentScale < 1 && currentScrollTop === 0) {
      e.preventDefault()
      const delta = Math.min(Math.abs(scrollAmount) / 100, 0.1)
      const newScale = Math.min(1, currentScale + delta)
      setScale(parseFloat(newScale.toFixed(2)))
      checkLocking(newScale)
      return
    }
  }
  // 绑定原生事件,设置passive为false
  appRef.current.addEventListener('wheel', handleWheel, { passive: false })
  return () => {
    appRef.current?.removeEventListener('wheel', handleWheel)
  }
}, [width])

const handleScroll = (e) => {
  const element = e.target;
  setScrollTop(element.scrollTop)
}

第三步:修正重复的className属性

<div 
  ref={appRef}
  className={`App ${scrolledOut ? "unlocked" : "locked"}`}
  onScroll={handleScroll}
  data-theme="dark"
>

第四步:补充CSS样式,确保缩放流畅

开启GPU加速缩放动画,避免布局抖动:

.App {
  transform-origin: top center;
  transition: transform 0.1s linear;
  will-change: transform;
}
.App.locked {
  overflow: hidden;
  height: 100vh;
}
.App.unlocked {
  overflow-y: auto;
}

优化建议

  • 可以用requestAnimationFrame包裹scale的更新逻辑,进一步提升动画流畅度,避免掉帧。
  • 缩放完成后可以给页面加一个微小的滚动偏移,避免用户继续滚动时出现突兀的切换感。

内容的提问来源于stack exchange,提问作者cmcnphp

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 18:45:03