- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
导读
useRafFn是 VueUse 中用于在每一帧(requestAnimationFrame)上调用回调函数的组合式函数,并提供暂停(pause)与恢复(resume)的精细控制。它在动画循环、帧率统计、实时数据更新、游戏循环等场景中非常实用,尤其适合需要高性能、节能的浏览器端逐帧任务。读完本文,你将掌握useRafFn的完整 API、所有配置项(immediate、fpsLimit、once)、回调参数(delta与timestamp)的语义,并能结合源码原理与测试用例,在实际项目中写出正确、可控、可复用的逐帧逻辑。
一、useRafFn 是什么
useRafFn的作用非常明确:在每个requestAnimationFrame上调用传入的函数,并提供暂停和恢复的控制能力。它是 VueUse Animation(动画)分类下的核心工具之一,位于 packages/core/useRafFn/index.ts,并已从 packages/core/index.ts 的入口统一导出,可通过以下方式直接使用:
import { useRafFn } from '@vueuse/core'与直接裸写requestAnimationFrame递归不同,useRafFn帮你封装了:
- 回调的递归调度(无需手动维护
rafId); - 暂停 / 恢复 / 自动清理(组件卸载时自动
cancelAnimationFrame); - 帧间隔(
delta)与帧时间戳(timestamp)计算; - 可选的帧率限制(
fpsLimit)与一次性执行(once)。
二、基础用法
官方文档(packages/core/useRafFn/index.md)给出的最小示例:
import { useRafFn } from '@vueuse/core' import { shallowRef } from 'vue' const count = shallowRef(0) const { pause, resume } = useRafFn(() => { count.value++ console.log(count.value) })这段代码会立刻开始逐帧执行回调(默认immediate: true),每帧让count加 1 并打印。调用pause()后循环暂停,调用resume()后从下一帧恢复。
仓库自带的演示组件 packages/core/useRafFn/demo.vue 展示了更贴近实战的用法——在 Vue 模板中展示当前帧数与帧间隔,并通过按钮控制暂停与恢复:
<script setup lang="ts"> import { useRafFn } from '@vueuse/core' import { shallowRef } from 'vue' const fpsLimit = 60 const count = shallowRef(0) const deltaMs = shallowRef(0) const { pause, resume } = useRafFn(({ delta }) => { deltaMs.value = delta count.value += 1 }, { fpsLimit }) </script> <template> <div font-mono> Frames: {{ count }} </div> <div font-mono> Delta: {{ deltaMs.toFixed(0) }}ms </div> <div font-mono> FPS Limit: {{ fpsLimit }} </div> <button @click="pause"> pause </button> <button @click="resume"> resume </button> </template>在这个示例中,回调通过解构拿到了delta(距上一帧的毫秒数),并将其渲染到页面上,同时用一个 60 的fpsLimit限制了执行频率。
三、回调参数:delta 与 timestamp
useRafFn的回调函数接收一个UseRafFnCallbackArguments类型的参数对象(见 packages/core/useRafFn/index.ts):
export interface UseRafFnCallbackArguments { /** * Time elapsed between this and the last frame. */ delta: number /** * Time elapsed since the creation of the web page. */ timestamp: DOMHighResTimeStamp }| 参数 | 类型 | 含义 |
|---|---|---|
delta | number | 本次回调与上一次回调之间的时间差(毫秒),可用于计算移动距离、速度等物理量 |
timestamp | DOMHighResTimeStamp | 页面创建以来流逝的时间(毫秒),即浏览器传入requestAnimationFrame回调的高精度时间戳(Time origin) |
值得注意的实现细节是:delta与浏览器原生requestAnimationFrame回调传入的时间戳并非同一个值。在 index.ts 的loop函数中,VueUse 会记录上一次的帧时间戳previousFrameTimestamp,再计算差值:
function loop(timestamp: DOMHighResTimeStamp) { if (!isActive.value || !window) return if (!previousFrameTimestamp) previousFrameTimestamp = timestamp const delta = timestamp - previousFrameTimestamp // ... 帧率限制判断 ... previousFrameTimestamp = timestamp fn({ delta, timestamp }) // ... }也就是说:第一帧执行时delta为 0(因为没有前一帧),从第二帧开始delta才反映真实的帧间隔。这一行为在 index.browser.test.ts 中通过fn.mock.calls[0][0]?.delta与fn.mock.calls[0][0]?.timestamp断言其存在性得到了验证。
四、配置项详解
useRafFn的第二个参数是UseRafFnOptions,包含三个核心配置项:
export interface UseRafFnOptions extends ConfigurableWindow { immediate?: boolean // @default true fpsLimit?: MaybeRefOrGetter<number | null> // @default null once?: boolean // @default false }4.1 immediate:是否立即开始
- 默认值:
true,即创建时立刻调用resume()开始循环(见 index.ts)。 - 设为
false时,循环不会自动开始,需要手动调用resume()启动。
典型场景:初始化数据尚未就绪,或希望按用户交互时机再启动动画时使用immediate: false。测试用例 index.browser.test.ts 验证了immediate: false时isActive.value为false。
4.2 fpsLimit:帧率限制
- 默认值:
null,不限制,跟随浏览器原生刷新率(通常 60Hz 或 120Hz)。 - 传入数字 N 时,回调每秒最多执行 N 次;传入
null则取消限制。 - 类型为
MaybeRefOrGetter<number | null>,意味着可以传入 ref 或 getter,实现运行期动态调节帧率。
底层实现(index.ts)将帧率换算为帧间隔阈值:
const intervalLimit = computed(() => { const limit = toValue(fpsLimit) return limit ? 1000 / limit : null })随后在loop中,若当前delta小于阈值(intervalLimit.value && delta < intervalLimit.value),则跳过本次回调、直接请求下一帧(index.ts)。注意这里的“跳过”指的是不执行回调,而requestAnimationFrame本身仍按屏幕刷新率触发。
测试用例验证了两个关键行为:
- 帧率 20 的回调调用次数少于帧率 60 的(index.browser.test.ts);
- 传入响应式 ref 后,动态把
fr.value从 60 改为 20,回调频率随之下降(index.browser.test.ts)。
4.3 once:只执行一次
- 默认值:
false,持续循环。 - 设为
true时,回调执行一次后自动停止,相当于“下一帧执行一次”。在 index.ts 中,执行完回调后会将isActive置为false、rafId置空并直接返回,不再请求下一帧。
测试用例 index.browser.test.ts 明确断言:once: true时回调恰好被调用 1 次,而不设置时调用次数大于 1。
4.4 window:自定义 window 实例
UseRafFnOptions继承自ConfigurableWindow(见 packages/core/_configurable.ts),因此还支持传入自定义的window实例,例如在 iframe 或测试环境中使用。默认值defaultWindow在客户端为window、在服务端(SSR)为undefined(packages/core/_configurable.ts)。由于defaultWindow在服务端为undefined,useRafFn在 SSR 环境下不会启动循环、也不会报错,天然具备服务端渲染安全性。
五、返回对象:Pausable 接口
useRafFn返回一个Pausable类型对象(接口定义见 packages/shared/utils/types.ts):
export interface Pausable { readonly isActive: Readonly<ShallowRef<boolean>> pause: Fn resume: Fn }| 成员 | 类型 | 说明 |
|---|---|---|
isActive | Readonly<ShallowRef<boolean>> | 只读浅响应式标记,true表示循环正在运行 |
pause | Fn | 暂停循环,内部调用cancelAnimationFrame(rafId)并取消调度 |
resume | Fn | 恢复循环,重置帧时间戳后重新发起requestAnimationFrame |
具体实现见 index.ts:
function resume() { if (!isActive.value && window) { isActive.value = true previousFrameTimestamp = 0 rafId = window.requestAnimationFrame(loop) } } function pause() { isActive.value = false if (rafId != null && window) { window.cancelAnimationFrame(rafId) rafId = null } } if (immediate) resume() tryOnScopeDispose(pause)几个重要的行为细节:
- 幂等性:
pause()在已暂停状态下重复调用是安全的(isActive已为false,rafId为null时不做任何事);resume()也只在未激活时才会重新启动循环。 - 自动清理:通过
tryOnScopeDispose(pause),当组件卸载或副作用作用域销毁时自动暂停循环,避免内存泄漏与无效帧调用。这也是 VueUse 系列组合式函数的通用最佳实践。 - 暂停后恢复会重置帧时间戳(
previousFrameTimestamp = 0),恢复后的第一帧delta会被重新初始化为 0,避免将暂停时长计入帧间隔造成异常大的delta。
isActive使用shallowRef存储、shallowReadonly暴露,保证了状态读取的高效性与只读安全性。整个循环状态机在测试 index.browser.test.ts 中得到了全面覆盖:暂停后isActive为false、恢复后为true,且immediate: false时手动resume()同样能正常激活。
六、源码级原理解读:一帧的生命周期
把上面各部分串起来,useRafFn的完整运行流程如下:
- 初始化:解构默认配置(
immediate = true、fpsLimit = null、window = defaultWindow、once = false),创建isActive与intervalLimit计算属性。 - 启动:若
immediate为真,调用resume()——置isActive为true、清零previousFrameTimestamp、发起window.requestAnimationFrame(loop)。 - 每帧回调(
loop):- 若
!isActive.value || !window,直接返回(防御性检查); - 初始化首帧
previousFrameTimestamp; - 计算
delta = timestamp - previousFrameTimestamp; - 若设置了
fpsLimit且delta < 1000 / fpsLimit,则仅请求下一帧、不执行回调(帧率限制逻辑); - 否则更新
previousFrameTimestamp、调用fn({ delta, timestamp }); - 若
once为真,停止循环;否则继续请求下一帧。
- 若
- 暂停 / 清理:
pause()通过cancelAnimationFrame取消已排队的帧;作用域销毁时tryOnScopeDispose(pause)兜底清理。
整个实现不依赖 Vue 的响应式系统做调度,仅用computed对fpsLimit做惰性求值(配合toValue支持 ref/getter 输入),因此运行开销极低,非常适合高帧率场景。
七、实战场景与最佳实践
场景 1:动画循环
用fpsLimit将 CPU 密集型动画限制在 30 FPS,兼顾流畅度与性能:
const { pause, resume } = useRafFn(({ delta }) => { progress.value = Math.min(progress.value + delta / 1000 * speed, 1) }, { fpsLimit: 30 })场景 2:按需启动(如进入视口才开启动画)
const { isActive, pause, resume } = useRafFn(callback, { immediate: false }) onMounted(() => resume()) onBeforeUnmount(() => pause()) // 组件卸载时自动清理(tryOnScopeDispose 已兜底)场景 3:下一帧执行一次(防抖式延迟)
const { resume } = useRafFn(updateLayout, { once: true, immediate: false }) // 需要时:把更新推迟到下一帧,合并同帧内的多次修改 resume()最佳实践建议
- 优先用
delta而非累加计数器:基于delta计算位移/进度可避免因帧率波动或暂停导致的进度失真。 - 不需要回调时及时
pause():requestAnimationFrame循环在后台标签页会被浏览器自动降频,但仍建议在隐藏或不再需要时主动暂停,节省电量。 - 善用
fpsLimit的动态性:传入 ref,可在低电量模式、画质设置变化等场景下运行时降帧。 - SSR 环境下无需特判:服务端
defaultWindow为undefined,循环不会启动,代码天然可安全执行。
八、总结
useRafFn是一个轻量、可控、可组合的逐帧执行工具,核心价值在于:
- 封装了
requestAnimationFrame的递归调度与cancelAnimationFrame清理; - 提供
pause/resume/isActive完整的Pausable控制面; - 通过
fpsLimit(支持响应式)实现帧率限制,通过once实现一次性执行; - 回调携带
delta与timestamp,便于编写帧率无关的逻辑; - 自动作用域清理 + SSR 安全,开箱即用。
其 API 类型声明见 packages/core/useRafFn/index.ts,使用文档见 packages/core/useRafFn/index.md,交互示例见 packages/core/useRafFn/demo.vue,行为验证见 packages/core/useRafFn/index.browser.test.ts。如果需要比“帧”更细粒度的定时控制,还可以参考同仓库的useIntervalFn、useRafFn的姊妹工具useTimestamp等组合式函数。
- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
相关推荐
VueUse useRafFn 完全指南:基于 requestAnimationFrame 的响应式动画循环与 FPS 控制
VueUse useRafFn 完全指南:基于 requestAnimationFrame 的响应式动画循环与 FPS 控制 useRafFn 是 VueUse
前端airi 项目实战:深入掌握 VueUse useRafFn 与 requestAnimationFrame 驱动的动画循环控制
airi 项目实战:深入掌握 VueUse useRafFn 与 requestAnimationFrame 驱动的动画循环控制 useRafFn 是 VueU
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染vue-echarts 动画性能调优:requestAnimationFrame 与帧优化
vue echarts 动画性能调优:requestAnimationFrame 与帧优化 在数据可视化项目中,你是否遇到过图表动画卡顿、数据更新延迟的问题?特
前端图表库数据可视化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考