深入浅出 requestAnimationFrame:让动画更流畅的利器

前言

在Web开发中,动画效果已经成为提升用户体验的重要组成部分。然而,实现流畅的动画并不是一件简单的事情。今天我们来聊聊JavaScript中一个专门为动画而生的API------requestAnimationFrame,它将帮助你创建更加流畅、高效的动画效果。

什么是requestAnimationFrame?

简单来说,requestAnimationFrame是浏览器提供的一个专门用于动画的API,它告诉浏览器你希望执行一个动画,并请求浏览器在下次重绘之前调用指定的函数来更新动画。

通俗易懂的比喻

想象一下,你和浏览器之间有这样的对话:

  • 你(开发者):"浏览器大哥,我有个动画想让你帮忙执行。"
  • 浏览器:"好的,不过我现在有点忙,等我准备好绘制下一帧画面的时候再叫你,这样可以吗?"
  • 你:"没问题!"

这就是requestAnimationFrame的工作方式------它不会在固定的时间间隔执行,而是在浏览器准备绘制下一帧时执行,这样就能保证动画与浏览器的绘制节奏同步。

为什么需要requestAnimationFrame?

requestAnimationFrame出现之前,我们通常使用setTimeoutsetInterval来实现动画:

javascript 复制代码
// 传统的setInterval方式
setInterval(function() {
    // 更新动画
}, 16); // 大约60fps

这种方式存在几个问题:

  1. ​时机不准确​:浏览器可能因为各种原因延迟执行
  2. ​资源浪费​:即使页面被隐藏或最小化,动画仍在后台运行
  3. ​性能问题​:可能导致过度绘制或丢帧

使用方法

基本语法

javascript 复制代码
let animationId;

function animate() {
    // 动画逻辑代码
    animationId = requestAnimationFrame(animate);
}

// 启动动画
animationId = requestAnimationFrame(animate);

// 停止动画
cancelAnimationFrame(animationId);

简单示例:往复移动一个小方块

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>往复移动一个小方块</title>
</head>
<style>
    #box {
        width: 50px;
        height: 50px;
        background-color: red;
        position: absolute;
        left: 0;
        top: 100px;
    }
</style>

<body>
    <div id="box"></div>
    <button onclick="startAnimation()">开始动画</button>
    <button onclick="stopAnimation()">停止动画</button>
</body>

<script>
    const box = document.getElementById('box');

    let animationId;
    let position = 0;
    let driect = 'right';

    function animate() {
        if (driect === 'right') {
            position += 2;
            if (position >= 400) {
                driect = 'left';
            }
        } else {
            position -= 2;
            if (position <= 0) {
                driect = 'right';
            }
        }
        box.style.left = position + 'px';
        animationId = requestAnimationFrame(animate);
    }

    // 开始动画
    function startAnimation() {
        if (!animationId) {
            animationId = requestAnimationFrame(animate);
        }
    }

    // 停止动画
    function stopAnimation() {
        if (animationId) {
            cancelAnimationFrame(animationId);
            animationId = null;
        }
    }
</script>

</html>

小球弹跳下落动画

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>复杂动画示例-小球弹跳下落</title>
</head>
<style>
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }

    .demo-container {
        background-color: white;
        border-radius: 10px;
        padding: 30px;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        margin-bottom: 30px;
    }

    .ball {
        width: 50px;
        height: 50px;
        background: #e74c3c;
        border-radius: 50%;
        position: relative;
        margin: 20px auto;
    }

    .controls {
        display: flex;
        gap: 10px;
        margin: 20px 0;
        justify-content: center;
    }

    button {
        padding: 10px 20px;
        background: #3498db;
        border: none;
        color: white;
        border-radius: 5px;
        cursor: pointer;
    }
</style>


<body>
    <div class="demo-container">
        <h2>自由落体 + 弹跳</h2>
        <div class="ball" id="ball"></div>
        <div class="controls">
            <button id="startBtn">开始动画</button>
            <button id="resetBtn">重置</button>
        </div>
    </div>

</body>
<script>
    // 创建动画类

    class BallAnimationClass {
        constructor() {
            this.position = 0; // 当前位置
            this.velocity = 0; // 当前速度
            this.gravity = 0.5; // 重力加速度
            this.damping = 0.8; // 能力损失系数

            this.isRuning = false;

            this.animationId = null;

            this.ball = document.getElementById('ball');
        }
        start() {
            if (this.isRuning) return;
            this.isRuning = true;
            this.animate();
        }

        reset() {
            this.isRuning = false;
            if (this.animationId) {
                cancelAnimationFrame(this.animationId);
            }
            this.position = 0;
            this.velocity = 0;

            this.ball.style.transform = `translateY(${this.position}px)`;
        }
        animate() {
            if (!this.isRuning) return;

            // 应用重力
            this.velocity += this.gravity;

            // 改变位置
            this.position += this.velocity;

            // 碰撞检测(地面在300px位置)
            if (this.position > 300) {
                this.position = 300;

                // 按0.8的系数减少下落高度
                this.velocity = -this.velocity * this.damping;

                if (Math.abs(this.velocity) < 0.5) {
                    this.velocity = 0;
                    this.isRuning = false;
                }
            }

            this.ball.style.transform = `translateY(${this.position}px)`;

            this.animationId = requestAnimationFrame(() => this.animate());
        }
    }

    const ballAnimation = new BallAnimationClass();

    document.getElementById('startBtn').addEventListener('click', () => {
        ballAnimation.start();
    })

    document.getElementById('resetBtn').addEventListener('click', () => {
        ballAnimation.reset();
    })

</script>

</html>

数字增加动画

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数字计数动画</title>
</head>
<style>
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }

    body {
        max-width: 600px;
        margin: 50px auto;
        padding: 20px;
        text-align: center;
        background: #f5f7fa;
    }

    h1 {
        color: #2c3e50;
        margin-bottom: 30px;
    }

    .counter {
        font-size: 4rem;
        font-weight: 600;
        color: #3498db;
        margin: 30px 0;
        transition: color 0.3s;
    }

    .controls {
        margin: 20px 0;
    }

    button {
        padding: 10px 20px;
        margin: 0 10px;
        font-size: 1rem;
        background: #3498db;
        color: white;
        border: none;
        border-radius: 5px;
        cursor: pointer;
        transition: background 0.3s;
    }

    button:hover {
        background: #2980b9;
    }

    button:disabled {
        background: #bdc3c7;
        cursor: not-allowed;
    }

    .status {
        margin-top: 20px;
        color: #7f8c8d;
    }
</style>


<body>
    <h1>数字计数动画示例</h1>

    <div class="counter" id="counter">0</div>

    <div class="controls">
        <button id="startBtn">开始计数</button>
        <button id="pauseBtn" disabled>暂停</button>
        <button id="resetBtn">重置</button>
    </div>

    <div class="status" id="status">状态:等待开始</div>
</body>
<script>
    // 创建动画类
    class NumberCounter {
        constructor(element, targetValue = 1000, duration = 2000) {
            this.element = element;
            this.targetValue = targetValue;
            this.duration = duration;

            this.currentValue = 0;
            this.startTime = null;
            this.animationId = null;

            this.isRuning = false;
        }
        start() {
            if (this.isRuning) return;

            this.isRuning = true;
            this.startTime = performance.now();
            this.animate();

            document.getElementById('status').textContent = "状态:计数中...";
            document.getElementById('startBtn').disabled = true;
            document.getElementById('pauseBtn').disabled = false;
        }

        pause() {
            this.isRuning = false;
            if (this.animationId) {
                cancelAnimationFrame(this.animationId);
            }
            document.getElementById('status').textContent = "状态:已暂停";
            document.getElementById('startBtn').disabled = false;
            document.getElementById('pauseBtn').disabled = true;
        }

        reset() {
            this.pause();
            this.currentValue = 0;
            this.updateDisplay();
            document.getElementById('status').textContent = "状态:已重置";
        }
        animate() {
            if (!this.isRuning) return;
            const currentTime = performance.now();

            // 计算从点击【开始计数】过去了多久
            const elapsed = currentTime - this.startTime;

            // 计算在duration中占据的比例
            const progress = Math.min(elapsed / this.duration, 1);

            // 缓动函数(y = 1- (1-x)^3):开始较慢,中间加速,结束时又变慢
            const easedProgress = 1 - Math.pow((1 - progress), 3);

            // 计算当前值
            this.currentValue = Math.round(this.targetValue * easedProgress);

            this.updateDisplay();

            if (progress < 1) {
                this.animationId = requestAnimationFrame(() => this.animate());
            } else {
                this.isRuning = false;
                document.getElementById('status').textContent = "状态:完成!";
                document.getElementById('startBtn').disabled = false;
                document.getElementById('pauseBtn').disabled = true;
            }
        }

        updateDisplay() {
            this.element.textContent = this.currentValue.toLocaleString();

            // 添加简单的颜色变化
            const percent = this.currentValue / this.targetValue;
            const hue = 210 - percent * 60;
            this.element.style.color = `hsl(${hue}, 70%, 50%)`;
        }
    }

    const counter = new NumberCounter(
        document.getElementById('counter'), 1234567, 3000
    );

    // 开始动画
    document.getElementById('startBtn').addEventListener('click', () => {
        counter.start();
    })

    // 动画暂停
    document.getElementById('pauseBtn').addEventListener('click', () => {
        counter.pause();
    })

    // 动画重置
    document.getElementById('resetBtn').addEventListener('click', () => {
        counter.reset();
    })

</script>

</html>
相关推荐
GIS瞧葩菜2 小时前
【无标题】
开发语言·前端·javascript·cesium
彭于晏爱编程2 小时前
关于表单,别做工具库舔狗
前端·javascript·面试
拉不动的猪2 小时前
什么是二义性,实际项目中又有哪些应用
前端·javascript·面试
烟袅2 小时前
LeetCode 142:环形链表 II —— 快慢指针定位环的起点(JavaScript)
前端·javascript·算法
Ryan今天学习了吗2 小时前
💥不说废话,带你上手使用 qiankun 微前端并深入理解原理!
前端·javascript·架构
Predestination王瀞潞2 小时前
Java EE开发技术(第六章:EL表达式)
前端·javascript·java-ee
掘金012 小时前
在 Vue 3 项目中使用 MQTT 获取数据
前端·javascript·vue.js
wyzqhhhh3 小时前
同时打开两个浏览器页面,关闭 A 页面的时候,要求 B 页面同时关闭,怎么实现?
前端·javascript·react.js
晴殇i3 小时前
从 WebSocket 到 SSE:实时通信的轻量化演进
前端·javascript