[Js进阶]requestAnimationFrame应用

1,045 阅读9分钟

[Js进阶]requestAnimationFrame应用

简介

requestAnimationFrame 是一个用于动画效果的 API,它使用浏览器的刷新率来执行回调函数,通常每秒钟执行 60 次。与 setTimeoutsetInterval 不同的是,requestAnimationFrame 被视为一个特殊的“宏任务”。

宏任务是一个在事件循环队列中排队等待执行的任务。与宏任务不同的是,“微任务”是一个在当前任务完成后立即执行的任务,它通常用于处理异步操作的结果。

在 JavaScript 中,异步操作通常是通过回调函数或 Promise 处理的。当异步操作完成后,回调函数将被添加到微任务队列中,以便在当前任务完成后立即执行。这意味着,微任务的执行顺序优先于下一个宏任务。

对于 requestAnimationFrame,由于它被视为一个宏任务,因此它的执行顺序优先于微任务。这意味着,如果在 requestAnimationFrame 回调函数中添加了微任务,那么这些微任务将在下一个宏任务(即下一个 requestAnimationFrame 回调函数)执行前被执行。

应用场景

requestAnimationFrame主要用于实现流畅的动画效果,它可以在浏览器的重绘周期内执行指定的函数,从而避免由于频繁的重绘导致的性能问题。除了动画之外,requestAnimationFrame还可以应用于以下场景:

  1. 实现平滑滚动效果。使用requestAnimationFrame可以在滚动过程中不断更新滚动位置,并且可以控制滚动的速度和加速度,从而实现更加自然和流畅的滚动效果。
  2. 实现倒计时效果。使用requestAnimationFrame可以控制倒计时的间隔和更新频率,并且可以在倒计时结束之后立即执行指定的函数,从而实现更加精确和可控的倒计时效果。
  3. 实现拖拽效果。使用requestAnimationFrame可以在拖拽过程中不断更新元素的位置,并且可以控制拖拽的速度和加速度,从而实现更加自然和流畅的拖拽效果。
  4. 实现页面的渐变效果。使用requestAnimationFrame可以在一段时间内不断更新页面元素的样式,并且可以控制渐变的速度和方向,从而实现更加自然和流畅的渐变效果。

......

需要注意的是,requestAnimationFrame并不是万能的,它也有一些局限性。例如,在一些需要高精度计算的场景下,requestAnimationFrame可能无法满足需求,此时可能需要使用其他更加精细的计时器或者事件来实现。

实现平滑滚动效果

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Smooth Scroll</title>
    <style>
      body {
        height: 8000px;
      }
      .scroll-btn {
        position: fixed;
        bottom: 20px;
        right: 20px;
        cursor: pointer;
      }
    </style>
  </head>
  <body>
    <button class="scroll-btn">Scroll to Bottom</button>
    <script>
      const btn = document.querySelector('.scroll-btn');
      const scrollHeight = document.body.scrollHeight;
      const screenHeight = window.innerHeight;

      function scrollToBottom() {
        const currentScroll = window.pageYOffset;
        const remainingScroll = scrollHeight - currentScroll - screenHeight;

        if (remainingScroll > 0) {
          window.scrollTo(0, currentScroll + remainingScroll / 20);
          requestAnimationFrame(scrollToBottom);
        }
      }

      btn.addEventListener('click', scrollToBottom);
    </script>
  </body>
</html>

在这个示例中,我们首先获取了页面的滚动高度和屏幕高度。然后,定义了一个scrollToBottom函数,这个函数会在每一帧中更新滚动位置,并且使用requestAnimationFrame函数注册下一次动画帧。在scrollToBottom函数中,我们首先计算出当前滚动位置和还剩余的滚动距离,然后根据剩余滚动距离计算出滚动的距离,并使用window.scrollTo函数实现滚动。最后,如果还有剩余的滚动距离,就使用requestAnimationFrame函数注册下一次动画帧,从而实现平滑滚动效果。

实现倒计时效果

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Countdown</title>
    <style>
      body {
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
      }
      #countdown {
        font-size: 48px;
      }
    </style>
  </head>
  <body>
    <div id="countdown"></div>
    <script>
      const countdown = document.querySelector('#countdown');
      let endTime = Date.now() + 10000; // 倒计时 10s

      function updateCountdown() {
        const remainingTime = endTime - Date.now();

        if (remainingTime > 0) {
          const remainingSeconds = Math.floor(remainingTime / 1000);
          countdown.innerText = remainingSeconds;
          requestAnimationFrame(updateCountdown);
        } else {
          countdown.innerText = '0';
        }
      }
    //   updateCountdown()
      requestAnimationFrame(updateCountdown);
    </script>
  </body>
</html>

在这个示例中,我们首先定义了一个endTime变量,表示倒计时结束的时间。然后,定义了一个updateCountdown函数,这个函数会在每一帧中更新倒计时,并且使用requestAnimationFrame函数注册下一次动画帧。在updateCountdown函数中,我们首先计算出当前的剩余时间,然后根据剩余时间计算出剩余秒数,并更新倒计时的显示。最后,如果还有剩余时间,就使用requestAnimationFrame函数注册下一次动画帧,从而实现倒计时效果。

实现拖拽效果

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Drag and Drop</title>
    <style>
      #drag {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 100px;
        height: 100px;
        background-color: #f00;
        cursor: move;
      }
    </style>
  </head>
  <body>
    <div id="drag"></div>
    <script>
      const drag = document.querySelector('#drag');
      let isDragging = false;
      let startX = 0;
      let startY = 0;

      function handleMouseDown(event) {
        isDragging = true;
        startX = event.clientX;
        startY = event.clientY;
      }

      function handleMouseMove(event) {
        if (isDragging) {
          const deltaX = event.clientX - startX;
          const deltaY = event.clientY - startY;
          const currentX = parseInt(drag.style.left) || 0;
          const currentY = parseInt(drag.style.top) || 0;

          drag.style.left = currentX + deltaX + 'px';
          drag.style.top = currentY + deltaY + 'px';

          startX = event.clientX;
          startY = event.clientY;

          requestAnimationFrame(handleMouseMove);
        }
      }

      function handleMouseUp() {
        isDragging = false;
      }

      drag.addEventListener('mousedown', handleMouseDown);
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    </script>
  </body>
</html>

在这个示例中,我们首先定义了一个可拖拽的元素drag,并且为它添加了鼠标样式和事件。然后,定义了三个函数handleMouseDownhandleMouseMovehandleMouseUp,分别处理鼠标按下、鼠标移动和鼠标抬起事件。在handleMouseDown函数中,我们记录下鼠标按下时的位置。在handleMouseMove函数中,我们首先判断鼠标是否按下,如果按下了,就计算出鼠标移动的距离,并根据距离更新元素的位置。最后,如果正在拖拽,就使用requestAnimationFrame函数注册下一次动画帧,从而实现平滑拖拽效果。在handleMouseUp函数中,我们记录下鼠标抬起的事件,表示拖拽结束。

实现渐变效果

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>渐变效果示例</title>
</head>
<body>
  <div id="myElement" style="width: 200px; height: 200px;"></div>

  <script>
    const element = document.getElementById('myElement'); // 获取需要渐变的元素
    let start = null; // 记录动画开始时间
    const duration = 10000; // 动画持续时间(单位:毫秒)
    const startColor = [255, 255, 255]; // 起始颜色(白色)
    const endColor = [0, 0, 0]; // 结束颜色(黑色)

    function animate(timestamp) {
      if (!start) start = timestamp; // 如果动画未开始,则记录开始时间
      const elapsed = timestamp - start; // 计算动画已经进行的时间
      const progress = Math.min(elapsed / duration, 1); // 计算动画进度(0-1)
      const color = interpolateColor(startColor, endColor, progress); // 计算当前颜色
      element.style.backgroundColor = `rgb(${color[0]}, ${color[1]}, ${color[2]})`; // 设置元素背景颜色

      if (progress < 1) {
        requestAnimationFrame(animate); // 如果动画未结束,则继续执行动画
      }
    }

    requestAnimationFrame(animate); // 开始执行动画

    function interpolateColor(startColor, endColor, progress) {
      const color = [];
      for (let i = 0; i < 3; i++) {
        color[i] = Math.round(startColor[i] + (endColor[i] - startColor[i]) * progress);
      }
      return color;
    }
  </script>
</body>
</html>

Vue中结合使用

以下是一个简单的自定义指令示例,用于在元素滚动时实现类似于 Parallax 效果

<template>
  <div class="parallax" v-parallax="{ speed: 0.2 }">
    <img src="image.jpg">
  </div>
</template>

<script>
export default {
  directives: {
    parallax: {
      bind: function (el, binding) {
        const speed = binding.value.speed || 0.1;
        let lastScrollTop = 0;
        let animationFrameId = null;

        function animate() {
          const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
          const offset = (scrollTop - lastScrollTop) * speed;
          lastScrollTop = scrollTop;
          el.style.transform = `translateY(${offset}px)`;
          animationFrameId = requestAnimationFrame(animate);
        }

        animate();

        el.__parallax_unbind__ = function() {
          cancelAnimationFrame(animationFrameId);
        };
      },
      unbind: function (el) {
        el.__parallax_unbind__();
        delete el.__parallax_unbind__;
      }
    }
  }
};
</script>

<style scoped>
.parallax {
  position: relative;
  height: 300px;
  overflow: hidden;
}

.parallax img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: auto;
}
</style>

在这个例子中,我们创建了一个名为 v-parallax 的指令,并传递了一个 speed 参数用于控制滚动速度。在指令的 bind 钩子中,我们使用 requestAnimationFrame 来实现动画效果,根据滚动的位置计算出偏移量,并将其应用于元素的 transform 属性。

在指令的 unbind 钩子中,我们清除了动画效果,以避免在元素被销毁时仍然继续执行动画。

在模板中,我们将指令应用于包含图片的 div 元素上,并设置了一些基本的样式。当页面滚动时,图片将以不同的速度向上或向下滚动,实现类似于 Parallax 效果。

注意,在这个示例中,我们使用了 window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop 来获取滚动的位置。这是因为不同的浏览器可能会使用不同的属性来表示滚动偏移量。

另外,由于 requestAnimationFrame 在每秒钟执行大约 60 次,因此它比使用 setTimeoutsetInterval 更加高效和流畅。

React中结合使用

实现一个逐渐变暗的背景效果

import React, { useRef, useEffect } from 'react';

function Background() {
  const backgroundRef = useRef(null);

  useEffect(() => {
    let requestId;
    let opacity = 1;

    function animate() {
      opacity -= 0.01;
      if (opacity < 0) {
        opacity = 0;
      }
      backgroundRef.current.style.opacity = opacity;
      if (opacity > 0) {
        requestId = requestAnimationFrame(animate);
      }
    }

    requestId = requestAnimationFrame(animate);

    return () => {
      cancelAnimationFrame(requestId);
    };
  }, []);

  return (
    <div
      ref={backgroundRef}
      style={{
        position: 'fixed',
        top: 0,
        left: 0,
        width: '100%',
        height: '100%',
        backgroundColor: '#000',
        opacity: 1
      }}
    />
  );
}

在这个例子中,我们使用 useRef 钩子创建了一个名为 backgroundRef 的引用,并将其绑定到一个 div 元素上。在 useEffect 钩子中,我们使用 requestAnimationFrame 实现了逐渐变暗的背景效果。

useEffect 钩子中,我们使用了一个 requestId 变量来存储 requestAnimationFrame 的返回值,以便在组件被卸载时可以清除它。我们还使用了一个 opacity 变量来存储背景的透明度,并在每次 animate 函数调用中减少它。当透明度小于 0 时,我们将其设置为 0,并停止调用 requestAnimationFrame

在组件的返回值中,我们将 backgroundRef 绑定到一个 div 元素上,并设置了一些基本的样式。在 useEffect 钩子中,我们使用 backgroundRef 的 current属性来获取该元素,并将其透明度设置为opacity` 变量的值。在初次渲染时,背景的透明度将被设置为 1。

当组件被卸载时,我们使用 cancelAnimationFrame 函数清除 requestAnimationFrame 的调用。这是为了防止在组件卸载后仍然继续执行动画。

与setTimeout比较

requestAnimationFramesetTimeout 都是用于异步执行代码的 JavaScript API。

异同点:

  1. 执行时机:setTimeout 函数会在指定的时间间隔后执行回调函数,而 requestAnimationFrame 则会在浏览器下一次重绘之前执行回调函数。通常情况下,浏览器的重绘频率是每秒钟 60 次,因此 requestAnimationFrame 通常比 setTimeout 更加高效、流畅。
  2. 回调函数参数:setTimeout 函数的回调函数会接收一个可选的参数,表示在指定的时间间隔后是否重复执行回调函数。而 requestAnimationFrame 的回调函数不接受任何参数。
  3. 取消执行:通过调用 clearTimeout 函数可以取消 setTimeout 的执行,而通过调用 cancelAnimationFrame 函数可以取消 requestAnimationFrame 的执行。
  4. 兼容性:setTimeout 是一个比较老的 API,在所有主流浏览器中都得到了支持。而 requestAnimationFrame 则是比较新的 API,在一些旧的浏览器中可能不被支持。

异同点总结:

相同点:都是用于异步执行代码的 JavaScript API。

不同点:

  1. 执行时机不同,setTimeout 在指定时间后执行回调函数,requestAnimationFrame 在浏览器下一次重绘前执行回调函数。

  2. 回调函数参数不同,setTimeout 的回调函数接受一个可选的参数,表示是否重复执行回调函数,requestAnimationFrame 不接受任何参数。

  3. 取消执行方式不同,clearTimeout 取消 setTimeout 的执行,cancelAnimationFrame 取消 requestAnimationFrame 的执行。

  4. 兼容性不同,setTimeout 在所有主流浏览器中都得到了支持,requestAnimationFrame 在一些旧的浏览器中可能不被支持。

    另外,需要注意的是,setTimeoutrequestAnimationFrame 适用于不同的场景。setTimeout 通常用于延迟执行一段代码,例如实现一个倒计时功能。而 requestAnimationFrame 则通常用于实现动画效果,因为它能够根据浏览器的刷新率自动调整动画的帧率,从而使得动画更加流畅。

与setInterval比较

异同点:

  1. 执行时机:setInterval 函数会在指定的时间间隔后重复执行回调函数,而 requestAnimationFrame 则会在浏览器下一次重绘之前执行回调函数。与 setTimeout 一样,requestAnimationFrame 通常比 setInterval 更加高效、流畅。
  2. 回调函数参数:setInterval 函数的回调函数会接收一个可选的参数,表示在指定的时间间隔后是否重复执行回调函数。而 requestAnimationFrame 的回调函数不接受任何参数。
  3. 取消执行:通过调用 clearInterval 函数可以取消 setInterval 的执行,而通过调用 cancelAnimationFrame 函数可以取消 requestAnimationFrame 的执行。
  4. 执行间隔:setInterval 函数的执行间隔是固定的,而 requestAnimationFrame 的执行间隔则会根据浏览器的刷新率自动调整。因此,requestAnimationFrame 更加适合实现动画效果,而 setInterval 更适合实现一些周期性的操作,例如定时发送心跳包等。

异同点总结:

相同点:都是用于周期性地执行代码的 JavaScript API。

不同点:

  1. 执行时机不同,setInterval 在指定时间间隔后重复执行回调函数,requestAnimationFrame 在浏览器下一次重绘前执行回调函数。

  2. 回调函数参数不同,setInterval 的回调函数接受一个可选的参数,表示是否重复执行回调函数,requestAnimationFrame 不接受任何参数。

  3. 取消执行方式不同,clearInterval 取消 setInterval 的执行,cancelAnimationFrame 取消 requestAnimationFrame 的执行。

  4. 执行间隔不同,setInterval 的执行间隔是固定的,而 requestAnimationFrame 的执行间隔则会根据浏览器的刷新率自动调整。

    另外,需要注意的是,setIntervalrequestAnimationFrame 适用于不同的场景。setInterval 通常用于定时执行一些简单的周期性操作,例如每隔一定时间打印一次日志。而 requestAnimationFrame 则通常用于实现动画效果,因为它能够根据浏览器的刷新率自动调整动画的帧率,从而使得动画更加流畅。