拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

VueUse useRafFn 完全指南:用 requestAnimationFrame 驱动高性能动画与帧回调

VueUse useRafFn 完全指南:用 requestAnimationFrame 驱动高性能动画与帧回调
  • 前端

【免费下载链接】vueuse

Collection of essential Vue Composition Utilities for Vue 3

项目地址:https://gitcode.com/gh_mirrors/vu/vueuse
点击查看免费下载

导读

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 }
参数类型含义
deltanumber本次回调与上一次回调之间的时间差(毫秒),可用于计算移动距离、速度等物理量
timestampDOMHighResTimeStamp页面创建以来流逝的时间(毫秒),即浏览器传入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 }
成员类型说明
isActiveReadonly<ShallowRef<boolean>>只读浅响应式标记,true表示循环正在运行
pauseFn暂停循环,内部调用cancelAnimationFrame(rafId)并取消调度
resumeFn恢复循环,重置帧时间戳后重新发起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)

几个重要的行为细节:

  1. 幂等性:pause()在已暂停状态下重复调用是安全的(isActive已为false,rafId为null时不做任何事);resume()也只在未激活时才会重新启动循环。
  2. 自动清理:通过tryOnScopeDispose(pause),当组件卸载或副作用作用域销毁时自动暂停循环,避免内存泄漏与无效帧调用。这也是 VueUse 系列组合式函数的通用最佳实践。
  3. 暂停后恢复会重置帧时间戳(previousFrameTimestamp = 0),恢复后的第一帧delta会被重新初始化为 0,避免将暂停时长计入帧间隔造成异常大的delta。

isActive使用shallowRef存储、shallowReadonly暴露,保证了状态读取的高效性与只读安全性。整个循环状态机在测试 index.browser.test.ts 中得到了全面覆盖:暂停后isActive为false、恢复后为true,且immediate: false时手动resume()同样能正常激活。

六、源码级原理解读:一帧的生命周期

把上面各部分串起来,useRafFn的完整运行流程如下:

  1. 初始化:解构默认配置(immediate = true、fpsLimit = null、window = defaultWindow、once = false),创建isActive与intervalLimit计算属性。
  2. 启动:若immediate为真,调用resume()——置isActive为true、清零previousFrameTimestamp、发起window.requestAnimationFrame(loop)。
  3. 每帧回调(loop):
    • 若!isActive.value || !window,直接返回(防御性检查);
    • 初始化首帧previousFrameTimestamp;
    • 计算delta = timestamp - previousFrameTimestamp;
    • 若设置了fpsLimit且delta < 1000 / fpsLimit,则仅请求下一帧、不执行回调(帧率限制逻辑);
    • 否则更新previousFrameTimestamp、调用fn({ delta, timestamp });
    • 若once为真,停止循环;否则继续请求下一帧。
  4. 暂停 / 清理: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

项目地址:https://gitcode.com/gh_mirrors/vu/vueuse
点击查看免费下载

相关推荐

上一篇:UniversalUnityDemosaics终极指南:3步实现Unity游戏马赛克高效移除
下一篇:本地多人游戏分屏工具完全指南:用Nucleus Co-Op实现4人同屏游戏

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表