Lazy loaded image
前端
React 数字动画 Hook 实战:用 requestAnimationFrame 创建平滑计数效果 (附源码与优化)
字数 2031阅读时长 6 分钟
2024-6-7
2025-5-6
type
status
date
slug
summary
tags
category
icon
password
Meta Description: 学习如何创建一个 React 自定义 Hook (useNumberAnimation),使用 requestAnimationFrame 实现从 0 平滑增长到目标数字的动画效果。包含完整代码、用法示例、性能优化技巧,提升网页交互体验。
(文章正文)

引言:告别生硬的数字跳转

在浏览数据看板、统计展示等类型的网页时,你是否注意到,很多数字并非瞬间从 0 跳变到最终值(例如 99999)?这种直接的变化显得有些生硬。取而代之的是一种平滑的数字增长动画效果,数值从起始点逐步、流畅地增加到目标值,如下图所示:
notion image
这种动画效果不仅视觉上更吸引人,也给用户一种动态、实时的感觉。那么,如何在 React 项目中实现这种效果呢?本文将带你一步步创建一个可复用的自定义 Hook (useNumberAnimation) 来轻松实现它。

核心思路:构建 useNumberAnimation Hook

我们将创建一个名为 use-number-animation.ts 的文件来封装这个逻辑。

1. 确定 Hook 的输入与输出 (API 设计)

一个健壮的 Hook 需要清晰的输入参数:
  • from (number): 动画开始时的数字,默认为 0。
  • to (number): 动画结束时的目标数字。
  • duration (number): 动画持续的总时间(毫秒),默认为 2000ms。
  • (更新) 我们将让 Hook 直接返回动画中的当前数字,而不是使用回调。
所以,Hook 的签名大致如下: useNumberAnimation(to: number, options?: { from?: number; duration?: number }): number

2. 动画引擎:requestAnimationFrame 的选择

数字动画的核心在于定时更新显示的数值。我们可能会首先想到 setInterval,但对于流畅的动画效果,requestAnimationFrame (rAF) 是更优的选择。
为什么选择 requestAnimationFrame?
  • 性能更佳: rAF 会在浏览器下一次重绘之前执行回调函数,确保动画与浏览器的刷新率同步(通常是 60fps),避免不必要的计算和渲染。
  • 更平滑: 相比 setInterval 可能出现的掉帧或卡顿,rAF 提供了更平滑、自然的动画过渡。
  • 资源优化: 当页面或标签页处于非活动状态时,浏览器会自动暂停 rAF 的执行,节省 CPU 资源。

3. 计算增长逻辑

动画的每一帧都需要计算出当前应该显示的数字。这基于已消耗时间总持续时间的比例。
  • 总增长量 (range): to - from
  • 动画开始时间 (startTime): 记录动画启动的精确时间戳。
  • 当前时间 (currentTime): 在 rAF 回调中获取当前时间戳。
  • 已消耗时间 (elapsedTime): currentTime - startTime
  • 进度比例 (progress): elapsedTime / duration (限制在 0 到 1 之间)
  • 当前值 (currentValue): from + range * progress

4. 动画终止条件

动画何时停止?当 elapsedTime 大于或等于 duration 时,动画就应该结束。此时,应确保显示的数字精确地等于目标值 to

最终代码实现 (use-number-animation.ts)

结合以上思路,我们可以编写出 useNumberAnimation Hook:

如何在 React 组件中使用

使用这个 Hook 非常简单:

注意事项与优化

  • 性能考量: requestAnimationFrame 本身性能良好。我们在 Hook 内部通过 useState 更新数字。React 会进行 Diffing,通常不会有大的性能问题。如果更新频率极高且伴随复杂计算,可考虑使用 useRef 存储数字,仅在必要时(如动画结束或特定间隔)触发状态更新,但这会牺牲动画的实时平滑性。对于数字展示,当前实现通常足够。
  • 浮点数精度: 计算 currentValue 时会产生浮点数。在显示时,通常需要使用 Math.round(), toFixed()toLocaleString() 进行格式化。
  • 避免重复渲染问题 (原文提及): 原文提到了因频繁 setState 相同值导致的 React 报错。在我们改进后的 Hook 中,由于状态由 Hook 内部管理,并且 React 自身会优化相同值的 state 更新,这个问题基本不会在 Hook 使用层面出现。如果你的组件在接收到 animatedNumber 后执行了非常耗时的操作,那需要对你组件的逻辑进行优化,而非 Hook 本身。
    • (原文图示参考)
    • (原文图示参考)
    • (在我们新的 Hook 设计中通常不需要在消费端做此判断)
  • 依赖项管理: useEffect 的依赖项数组 ([to, from, duration]) 很重要,确保在这些关键参数变化时,动画能正确地重新启动或更新。

最终效果

通过使用 useNumberAnimation Hook,你可以轻松地在你的 React 应用中实现平滑的数字增长动画效果,提升用户界面的动态感和吸引力。
notion image

总结

本文介绍了一种使用 React 自定义 Hook 和 requestAnimationFrame 来创建数字增长动画的方法。这种方式代码简洁、可复用性强,并且性能良好。下次当你需要展示动态变化的数字时,不妨试试这个方法,给你的用户带来更流畅、愉悦的视觉体验!
上一篇
Sunshine-Track:轻量级前端监控库,适用于 Vue、React、Angular (错误、性能、行为追踪)
下一篇
纯 CSS 实现酷炫卷轴滚动动画 (联动页面滚动,含 JS 兼容方案)