原创 天天鸭 2024-12-24 08:30 重庆
点击关注公众号,“技术干货” 及时达!
前言
如果你是一名前端开发,那么你多少有了解过requestAnimationFrame吧?如果没有也接着往下看,会有详细用法说明。
其实很多人会局限于把requestAnimationFrame应用于一些纯动画相关的需求上,但其实在前端很多业务场景下requestAnimationFrame都能用于性能优化,下面将细说一下requestAnimationFrame的具体用法和几种应用场景。
requestAnimationFrame作用与用法
requestAnimationFrame简述
「MDN官方说法是这样的」
基本示例
<script lang="ts" setup>function init() {console.log('您好,我是requestAnimationFrame');}requestAnimationFrame(init)</script>
「效果如下」
但是例子上面是最基本的调用方式,并且只简单执行了一次,而对于动画是要一直执行的。
下面直接上图看看官方的文档对这个的说明,上面说具体用法应该要递归调用,而不是单次调用。
递归调用示例
<script lang="ts" setup>function init() {console.log('您好,递归调用requestAnimationFrame');requestAnimationFrame(init)}requestAnimationFrame(init)</script>
「执行动图效果如下」
requestAnimationFrame会一直递归调用执行,并且调用的频率通常是与当前显示器的刷新率相匹配(这也是这个API核心优势),例如屏幕75hz就1秒执行75次。
而且如果使用的是定时器实现此功能是无法适应各种屏幕帧率的。
回调函数
requestAnimationFrame执行后的回调函数有且只会返回一个参数,并且返回的参数是一个毫秒数,这个参数所表示是的上一帧渲染的结束时间,直接看看下面代码示例与打印效果。
<script lang="ts" setup>function init(val) {console.log('您好,requestAnimationFrame回调:', val);requestAnimationFrame(init);}requestAnimationFrame(init);</script>
「注意:」 如果我们同时调用了很多个requestAnimationFrame,那么他们会收到相同的时间戳,因为与屏幕的帧率相同所以并不会不一样。
终止执行
终止此API的执行,官方提供的方法是window.cancelAnimationFrame(),语法如下
ancelAnimationFrame(requestID)
直接看示例更便于理解,用法非常类似定时器的clearTimeout(),直接把 requestAnimationFrame 返回值传给 cancelAnimationFrame() 即可终止执行。
<template><div><button @click="stop">停止</button></div></template><script lang="ts" setup>let myReq;function init(val) {console.log('您好,requestAnimationFrame回调:', val);myReq = requestAnimationFrame(init);}requestAnimationFrame(init);function stop() {cancelAnimationFrame(myReq);}</script>
requestAnimationFrame优势
动画更丝滑,不会出现卡顿
对比传统的setTimeout 和 setInterval动画会更流畅丝滑。
主要 「原因」 是由于运行的浏览器会监听显示器返回的VSync信号确保同步,收到信号后再开始新的渲染周期,因此做到了与浏览器绘制频率绝对一致。所以帧率会相当平稳,例如显示屏60hz,那么会固定1000/60ms刷新一次。
但如果使用的是setTimeout 和 setInterval来实现同样的动画效果,它们会受到事件队列宏任务、微任务影响会导致执行的优先级顺序有所差异,自然做不到与绘制同频。
所以使用setTimeout 和 setInterval不但无法自动匹配显示屏帧率,也无法做到完全固定的时间去刷新。
性能更好,切后台会暂停
当我们把使用了requestAnimationFrame的页面切换到后台运行时,requestAnimationFrame会暂停执行从而提高性能,切换回来后会马上提着执行。
效果如下动图,隐藏后停止运行,切换回来接着运行。
应用场景:常规动画
用一个很简单的示例:用requestAnimationFrame使一张图片动态也丝滑旋转,直接看示例代码和效果。
「思路」:首先在页面初始化时执行window.requestAnimationFrame(animate)使动画动起来,实现动画一直丝滑转运。在关闭页面时用window.cancelAnimationFrame(rafId)去终止执行。
<template><div class="container"><div :style="imgStyle" class="earth"></div></div></template><script setup>import { ref, onMounted, reactive, onUnmounted } from 'vue';const imgStyle = reactive({transform: 'rotate(0deg)',});let rafId = null;// 请求动画帧方法function animate(time) {const angle = (time % 10000) / 5; // 控制转的速度imgStyle.transform = `rotate(${angle}deg)`;rafId = window.requestAnimationFrame(animate);}// 开始动画onMounted(() => {rafId = window.requestAnimationFrame(animate);});// 卸载时生命周末停止动画onUnmounted(() => {if (rafId) {window.cancelAnimationFrame(rafId);}});</script><style scoped>body {box-sizing: border-box;background-color: #ccc;height: 100vh;display: flex;justify-content: center;align-items: center;}.container {position: relative;height: 100%;width: 100%;display: flex;flex-direction: column;align-items: center;justify-content: center;}.earth {height: 100px;width: 100px;background-size: cover;border-radius: 50%;background-image: url('@/assets/images/about_advantage_3.png'); /* 替换为实际的路径 */}</style>
看看动图效果
应用场景:滚动加载
在滚动事件中用requestAnimationFrame去加载渲染数据使混动效果更加丝滑。主要好久有几个
「提高性能」: 添加requestAnimationFrame之后会在下一帧渲染之前执行,而不是每次在滚动事件触发的时候就立即执行。这可以减少大量不必要的计算,提高性能。
「用户体验更好」:确保在绘制下一帧时再执行,使帧率与显示屏相同,视觉上会更丝滑。
代码示例和效果如下:
<template><div class="container" ref="scrollRef"><div v-for="(item, index) in items" :key="index" class="item">{{ item }}</div><div v-if="loading" class="loading">数据加载中...</div></div></template><script setup lang="ts">import { ref, onMounted, onUnmounted } from 'vue';const loading = ref(false);let rafId: number | null = null;// 数据列表const items = ref<string[]>(Array.from({ length: 50 }, (_, i) => `Test ${i + 1}`));// 滚动容器const scrollRef = ref<HTMLElement | null>(null);// 模拟一个异步加载数据效果const moreData = () => {return new Promise<void>((resolve) => {setTimeout(() => {const newItems = Array.from({ length: 50 }, (_, i) => `Test ${items.value.length + i + 1}`);items.value.push(...newItems);resolve();}, 1000);});};// 检查是否需要加载更多数据const checkScrollPosition = () => {if (loading.value) return;const container = scrollRef.value;if (!container) return;const scrollTop = container.scrollTop;const clientHeight = container.clientHeight;const scrollHeight = container.scrollHeight;if (scrollHeight - scrollTop - clientHeight <= 100) {startLoading();}};// 加载数据const startLoading = async () => {loading.value = true;await moreData();loading.value = false;};// 监听滚动事件const handleScroll = () => {console.log('滚动事件触发啦');if (rafId !== null) {window.cancelAnimationFrame(rafId);}rafId = window.requestAnimationFrame(checkScrollPosition);};// 添加滚动事件监听器onMounted(() => {if (scrollRef.value) {scrollRef.value.addEventListener('scroll', handleScroll);}});// 移除相关事件onUnmounted(() => {if (rafId !== null) {window.cancelAnimationFrame(rafId);}if (scrollRef.value) {scrollRef.value.removeEventListener('scroll', handleScroll);}});</script><style scoped>.container {padding: 20px;max-width: 800px;overflow-y: auto;margin: 0 auto;height: 600px;}.item {border-bottom: 1px solid #ccc;padding: 10px;}.loading {padding: 10px;color: #999;text-align: center;}</style>
看看下面动图效果
小结
通过代码示例配合动图讲解后,再通过两个简单的事例可能大家会发现,只要在页面需要运动的地方其实都可以用到 requestAnimationFrame 使效果变的更加丝滑。
除了上面两个小示例其它非常多地方都可以用到requestAnimationFrame去优化性能,比较常见的例如游戏开发、各种动画效果和动态变化的布局等等。
❝文章就写到这啦,如果文章写的哪里不对或者有什么建议欢迎指出。
❞
点击关注公众号,“技术干货” 及时达!
