news 2026/9/19 3:49:28

react-use 的 useRaf Hook:基于 requestAnimationFrame 的动画进度驱动方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
react-use 的 useRaf Hook:基于 requestAnimationFrame 的动画进度驱动方案

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(如useRafLoopuseRafState)之间的区别。

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组件持续重新渲染的毫秒时长,即动画的总时长1e12
delaynumber延迟多少毫秒后开始渲染,即动画开始的等待时间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] = useState<number>(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);

整个时序如下:

  1. 副作用执行后,立刻注册timerDelay = setTimeout(onStart, delay),等待delay毫秒;
  2. onStart被触发:记录start = Date.now()作为计时起点,启动 RAF 循环,同时注册一个时长为mstimerStop
  3. timerStop到期(即到达总时长),取消当前 RAF 循环,并把状态强制置为 1,动画定格在终点;
  4. 组件卸载时,清理函数依次clearTimeout(timerStop)clearTimeout(timerDelay)cancelAnimationFrame(raf),确保不产生内存泄漏和卸载后的状态更新。

清理逻辑的测试佐证

tests/useRaf.test.ts 中的should clear pending timers on unmount用例专门验证了这一点:卸载组件后,clearTimeout恰好被调用 2 次(分别清理timerStoptimerDelay),cancelAnimationFrame被调用 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的返回值自行加工。

使用注意事项

  1. 副作用参数变化会重置动画:由于副作用依赖[ms, delay],运行时修改这两个参数会取消当前动画并从头开始,实际使用中建议将时长视为常量。
  2. 适合浏览器环境:尽管useIsomorphicLayoutEffect保证了 SSR 安全,但真正的帧循环只在浏览器端发生;服务端渲染阶段返回值恒为初始值 0。
  3. 与同类 Hook 的区别useRafLoop(见 src/useRafLoop.ts)关注的是"持续执行回调函数"而非返回进度值;useRafState(见 src/useRafState.ts)则是把状态更新节流到 RAF 帧内,而非按时间比例推进。三者定位不同,按需选择。
  4. 导出入口useRafuseRafLoopuseRafState一起从 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 3:48:00

Spring Boot+Spark气象数据分析毕设全流程实战指南

我最近在毕设资源库里翻项目&#xff0c;发现各种“Spring Boot Spark”的组合标题特别多&#xff0c;比如“基于Spark的西南天气数据的分析与应用”&#xff0c;几乎每隔几页就能看到一次。这个现象背后其实有很实际的原因&#xff1a;Spring Boot负责业务接口&#xff0c;Sp…

作者头像 李华
网站建设 2026/9/19 3:47:58

底座 WeKnora 管知识,TaoToken 替 ReAct Agent 管 Token

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 3:47:41

技术选型实战:从约束倒推架构,Electron+Agent案例复盘

1. 从业务约束倒推技术栈&#xff1a;没有最好的技术&#xff0c;只有最匹配的组合做技术选型最怕什么&#xff1f;不是候选方案不够多&#xff0c;而是评审会上所有人都在聊“这个框架性能好”“那个中间件社区火”&#xff0c;却没人先说清楚&#xff1a;我们的业务到底要解决…

作者头像 李华
网站建设 2026/9/19 3:47:05

搜索性能评估四锚点:数据规模、查询特征、SLA与运维水位

1. 为什么“比ES快5倍”这个说法本身就需要被拆解“推荐一个比ES快5倍的搜索引擎”——这句话在技术社区里像一枚扔进水塘的石子&#xff0c;涟漪一圈圈扩散&#xff0c;但很少有人蹲下来捞起那块石头&#xff0c;看看它到底是什么质地。我从2014年开始做搜索相关系统&#xff…

作者头像 李华