
react-use 的 useRaf Hook基于 requestAnimationFrame 的动画进度驱动方案【免费下载链接】react-useReact Hooks — 项目地址: https://gitcode.com/gh_mirrors/re/react-use导读useRaf是 react-use 提供的一个动画类 Hook它借助浏览器原生的requestAnimationFrame驱动组件在每个动画帧上重新渲染并返回一个 0 到 1 之间的数值表示自动画开始以来流逝时间占总时长的百分比。本文将以官方文档 docs/useRaf.md 为主体结合 src/useRaf.ts 的源码实现与 tests/useRaf.test.ts 的测试用例完整讲解其用法、参数语义、底层运行机制、清理逻辑及典型应用场景。读完本文你将能够在自己的 React 组件中直接使用useRaf构建进度条、倒计时、基于时间线的动画等能力并理解它与其他 RAF 类 Hook如useRafLoop、useRafState之间的区别。useRaf 是什么官方文档对useRaf的定位非常清晰一个 React 动画 Hook它强制组件在每一次requestAnimationFrame时重新渲染并返回已流逝时间的百分比。import {useRaf} from react-use; const Demo () { const elapsed useRaf(5000, 1000); return ( div Elapsed: {elapsed} /div ); };在上面的示例中组件挂载 1 秒delay后才开始计时随后在 5 秒ms的窗口期内每帧都会重新渲染elapsed从 0 平滑地增长到 1。elapsed本身就是一个可直接用于驱动 UI 的进度值比如把它渲染成进度条的宽度百分比、透明度或位移量。函数签名与参数详解官方文档给出了useRaf的类型签名useRaf(ms?: number, delay?: number): number;参数类型说明默认值msnumber组件持续重新渲染的毫秒时长即动画的总时长1e12delaynumber延迟多少毫秒后开始渲染即动画开始的等待时间0返回值number已流逝时间占总时长的百分比范围恒为[0, 1]0参数的实际语义从 src/useRaf.ts 的默认值可以看出ms默认取1e12约 31.7 年。这意味着如果不传任何参数useRaf()会在近乎无限的时间内持续逐帧返回一个不断增大的进度值——其行为更接近于无限循环的时间比例指示器这也是官方文档将其作为默认值的原因。ms决定动画窗口的总时长。百分比计算公式为(Date.now() - start) / ms其中start是动画实际开始的时刻。delay决定动画开始前的等待时间。实现中通过setTimeout(onStart, delay)来推迟启动延迟期间返回值恒为 0。源码实现逐帧推进的内部机制useRaf的实现非常精简完整逻辑位于 src/useRaf.ts。其核心思路是用一个状态保存当前进度用一个 RAF 递归循环持续更新它再用两个定时器分别控制开始与结束。状态与副作用时机const useRaf (ms: number 1e12, delay: number 0): number { const [elapsed, set] useStatenumber(0); useIsomorphicLayoutEffect(() { // ... }, [ms, delay]); return elapsed; };这里使用了useIsomorphicLayoutEffect见 src/useIsomorphicLayoutEffect.ts它在浏览器环境等价于useLayoutEffect在服务端渲染SSR环境下退化为useEffect因此useRaf可以安全地用于同构应用不会在服务端抛出requestAnimationFrame is not defined之类的错误。副作用依赖数组为[ms, delay]意味着当这两个参数任一变化时动画会重新初始化重置进度、重新计时。帧循环与进度计算const onFrame () { const time Math.min(1, (Date.now() - start) / ms); set(time); loop(); }; const loop () { raf requestAnimationFrame(onFrame); };每一帧执行onFrame用当前时刻减去start得到流逝时间除以ms得到进度比例并通过Math.min(1, ...)将结果钳制在 1 以内即进度永远不会超过 1随后立即调度下一帧形成持续到组件卸载或动画结束的循环。启动延迟与结束收尾const onStart () { timerStop setTimeout(() { cancelAnimationFrame(raf); set(1); }, ms); start Date.now(); loop(); }; const timerDelay setTimeout(onStart, delay);整个时序如下副作用执行后立刻注册timerDelay setTimeout(onStart, delay)等待delay毫秒onStart被触发记录start Date.now()作为计时起点启动 RAF 循环同时注册一个时长为ms的timerStop当timerStop到期即到达总时长取消当前 RAF 循环并把状态强制置为 1动画定格在终点组件卸载时清理函数依次clearTimeout(timerStop)、clearTimeout(timerDelay)、cancelAnimationFrame(raf)确保不产生内存泄漏和卸载后的状态更新。清理逻辑的测试佐证tests/useRaf.test.ts 中的should clear pending timers on unmount用例专门验证了这一点卸载组件后clearTimeout恰好被调用 2 次分别清理timerStop与timerDelaycancelAnimationFrame被调用 1 次清理未完成的帧请求。行为特性从测试用例看返回值规律仓库中的测试用例 tests/useRaf.test.ts 将useRaf的行为刻画得非常精确可作为理解该 Hook 的第一手依据初始值为 0无论是否传入参数Hook 首次渲染返回的进度值都是 0测试should init percentage of time elapsed。进度按比例递增测试使用raf-stub替换requestAnimationFrame、用 Jest fake timers 与Date.nowmock 推进时间验证了默认ms下进度依次为 0.25、0.5、0.75、1对应should return corresponding percentage of time elapsed for default ms自定义ms 2000时同样精确返回 25%、50%、75%、100%should return corresponding percentage of time elapsed for custom ms。这从侧面印证了返回值计算公式的线性特征progress (Date.now() - start) / ms。超过时长后恒为 1测试should return always 1 after corresponding ms reached表明当流逝时间达到总时长的 110%、300% 时返回值依然被钳制为 1——这正是Math.min(1, ...)起的作用进度永远不可能超过 1。延迟期间保持 0测试should wait until delay reached to start calculating elapsed percentage验证useRaf(undefined, 500)在 250ms、499ms 时返回值均为 0只有快进到恰好 500ms 后进度才开始增长说明delay期间完全不启动帧循环。典型应用驱动进度与动画useRaf的返回值天然适合作为动画进度输入。最直接的用法是进度条import {useRaf} from react-use; const ProgressBar ({duration 3000}) { const progress useRaf(duration); return ( div style{{width: 100%, background: #eee}} div style{{ width: ${progress * 100}%, height: 8, background: #3b82f6, transition: none, }} / /div ); };由于进度在每个 RAF 帧都会更新组件会以约 60fps取决于设备刷新率的频率重渲染进度条随之平滑推进。你还可以把返回值映射到任意 CSS 属性上例如透明度opacity: 1 - progress、位移transform: translateX(${progress * 100}px)等。需要延迟启动的倒计时场景也很自然const Countdown () { // 3 秒后开始持续 5 秒 const remaining 1 - useRaf(5000, 3000); return div剩余比例{remaining.toFixed(2)}/div; };与 useTween 的关系更上层的动画构建块useRaf在仓库内部还承担着更高级动画 Hook 的底层角色。以 src/useTween.ts 为例它直接调用useRaf(ms, delay)取得线性进度再将其传入ts-easing提供的缓动函数得到带缓动效果的动画值const useTween (easingName: string inCirc, ms: number 200, delay: number 0): number { const fn: Easing easing[easingName]; const t useRaf(ms, delay); // ... return fn(t); };从源码结构可以推断useRaf提供了时间 → 线性进度的通用映射而useTween等 Hook 在其之上叠加缓动曲线从而复用了帧循环与定时清理的全部基础设施。如果你需要实现自定义的缓动、往返动画或时间轴动画也可以基于useRaf的返回值自行加工。使用注意事项副作用参数变化会重置动画由于副作用依赖[ms, delay]运行时修改这两个参数会取消当前动画并从头开始实际使用中建议将时长视为常量。适合浏览器环境尽管useIsomorphicLayoutEffect保证了 SSR 安全但真正的帧循环只在浏览器端发生服务端渲染阶段返回值恒为初始值 0。与同类 Hook 的区别useRafLoop见 src/useRafLoop.ts关注的是持续执行回调函数而非返回进度值useRafState见 src/useRafState.ts则是把状态更新节流到 RAF 帧内而非按时间比例推进。三者定位不同按需选择。导出入口useRaf与useRafLoop、useRafState一起从 src/index.ts 统一导出可直接通过import { useRaf } from react-use引入仓库内的演示代码位于 stories/useRaf.story.tsx可在 Storybook 中交互查看效果。小结useRaf用约 30 行代码实现了按帧推进、按时长钳制、按延迟启动、按卸载清理的完整动画进度闭环ms控制总时长、delay控制启动时机、返回值恒在[0, 1]区间。无论是直接驱动进度条与 CSS 动画还是作为useTween等高级 Hook 的时间基底它都是 react-use 动画体系中简洁而关键的一环。【免费下载链接】react-useReact Hooks — 项目地址: https://gitcode.com/gh_mirrors/re/react-use创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考