【总结】2259- requestAnimationFrame的用法、优势和应用场景

科技   2024-11-29 23:10   福建  
前言

如果你是一名前端开发,那么你多少有了解过requestAnimationFrame吧?如果没有也接着往下看,会有详细用法说明。

很多人会局限于把requestAnimationFrame应用于一些纯动画相关的需求上,但其实在前端很多业务场景下requestAnimationFrame都能用于性能优化,下面将细说一下requestAnimationFrame的具体用法和几种应用场景

requestAnimationFrame作用与用法

requestAnimationFrame简述

MDN官方说法是这样的

基本示例

<script lang="ts" setup>
    function init({
      console.log('您好,我是requestAnimationFrame');
    }
    requestAnimationFrame(init)
</script>

效果如下

但是例子上面是最基本的调用方式,并且只简单执行了一次,而对于动画是要一直执行的。

如下图所示,官方的文档对这个有说明,具体用法应该要递归调用。

image.png

递归调用示例

<script lang="ts" setup>
    function init({
      console.log('您好,递归调用requestAnimationFrame');
      requestAnimationFrame(init)
    }
    requestAnimationFrame(init)
</script>

效果如下

requestAnimationFrame会一直执行,并且调用的频率通常与当前显示器的刷新率相匹配(这也是这个API核心优势),例如屏幕75hz1秒执行75次,而且如果使用的是定时器是无法适应各种屏幕帧率的。

动画.gif

回调函数

requestAnimationFrame执行后的回调函数有且只会返回一个参数,并且返回的参数是一个毫秒数,表示上一帧渲染的结束时间,直接看看下面示例。

<script lang="ts" setup>
  function init(val{
    console.log('您好,requestAnimationFrame回调:', val);
    requestAnimationFrame(init);
  }
  requestAnimationFrame(init);
</script>

image.png

注意: 如果我们同时调用多个requestAnimationFrame,那么他们会收到相同的时间戳。

终止执行

官方提供的方法是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>

动画.gif

requestAnimationFrame优势

1、动画更丝滑,不会出现卡顿

对比传统的setTimeoutsetInterval会更流畅丝滑。

由于requestAnimationFrame始终运行在js的主线程当中,因此做到了与浏览器绘制频率绝对一致。所以帧率会相当平稳,例如显示屏60hz,那么会固定1000/60ms刷新一次。

但如果使用的是setTimeoutsetInterval,它们会受到事件队列宏任务、微任务影响会导致执行的优先级顺序有所差异,自然做不到与绘制同频。所以使用setTimeoutsetInterval不但无法自动匹配显示屏帧率,也无法做到完全固定的时间去刷新。

2、性能更好,切后台会暂停

当我们切换页面后台运行时,requestAnimationFrame会暂停执行从而提高性能。

效果如下动图,隐藏后停止运行,切换回来接着运行。

动画.gif

应用场景:常规动画

用一个很简单的示例:用requestAnimationFrame使一张图片动态也丝滑旋转,直接看示例代码和效果。

<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;
    height100vh;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .container {
    position: relative;
    height100%;
    width100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }

  .earth {
    height100px;
    width100px;
    background-size: cover;
    border-radius50%;
    background-imageurl('@/assets/images/about_advantage_3.png'); /* 替换为实际的路径 */
  }
</style>

在生命周期中用递归调用requestAnimationFrame,实现动画一直丝滑转运,但在销毁时要用cancelAnimationFrame终止执行。

应用场景:滚动加载

在滚动事件中用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({ length50 }, (_, 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 {
  padding20px;
  max-width800px;
  overflow-y: auto;
  margin0 auto;
  height600px
}

.item {
  border-bottom1px solid #ccc;
  padding10px;
}

.loading {
  padding10px;
  color#999;
  text-align: center;
}
</style>
动画3.gif

小结

通过两个简单的事例可能大家会发现,只要在页面需要运动的地方其实都可以用到 requestAnimationFrame 使效果变的更加丝滑。例如游戏开发、各种动画效果和动态变化的布局等等。

文章就写到这啦,如果文章写的哪里不对或者有什么建议欢迎指出。


原文:https://juejin.cn/post/7431004279819288613

前端自习课
每日清晨,享受一篇前端优秀文章。
 最新文章