JavaScript 动画:从 setInterval 到 requestAnimationFrame 与可复用的 animate 时序框架(zh.javascript.info 实战指南)
文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载本篇技术指南围绕《现代 JavaScript 教程》zh.javascript.info中 JavaScript 动画一章展开讲解如何用原生 JavaScript 实现 CSS 无法完成的动画——例如沿任意时序函数运动的复杂路径、画布动画等。你将掌握基于requestAnimationFrame的动画原理、一套可复用的animate({timing, draw, duration})时序框架、五种常用时序函数幂次、圆弧、反弹、弹跳、伸缩及其easeOut/easeInOut变换并看到仓库中对应的可运行演示与练习源码。为什么需要 JavaScript 动画从 HTML/CSS 的角度看动画的本质是style 属性的逐渐变化。比如把style.left从0px逐步改为100px元素就会移动。但 CSS 动画transition/keyframes受限于 Bezier 曲线定义的时序无法精确表达“先拉弓再射出”“落体后多次弹跳”这类复杂运动也无法驱动画布canvas上的绘制逻辑。JavaScript 动画可以处理这些 CSS 无法处理的事情沿着与 Bezier 曲线不同的时序函数定义的复杂路径移动实现画布上的动画在draw回调中做任何事——不仅仅是改 CSS 属性还可以动态创建元素如烟花效果。仓库中的文章正文位于 7-animation/3-js-animation/article.md配套的可运行演示都放在同目录的.view子目录中下文将结合这些源码逐一展开。使用 setInterval最朴素的动画实现如果每秒做 50 次小变化动画看起来就会流畅。电影也是同样的原理每秒 24 帧或更多帧足以让人眼感觉平滑。伪代码如下let delay 1000 / 50; // 每秒 50 帧 let timer setInterval(function() { if (animation complete) clearInterval(timer); else increase style.left }, delay)仓库中 move.view/index.html 提供了一个完整的 setInterval 动画示例点击小火车图片让它用 2 秒时间从0px移动到400px。train.onclick function() { let start Date.now(); let timer setInterval(function() { let timePassed Date.now() - start; train.style.left timePassed / 5 px; if (timePassed 2000) clearInterval(timer); }, 20); }把这段代码抽象成一般形式就是文章中的标准写法let start Date.now(); // 保存开始时间 let timer setInterval(function() { // 距开始过了多长时间 let timePassed Date.now() - start; if (timePassed 2000) { clearInterval(timer); // 2 秒后结束动画 return; } // 在 timePassed 时刻绘制动画 draw(timePassed); }, 20); // 随着 timePassed 从 0 增加到 2000 // 将 left 的值从 0px 增加到 400px function draw(timePassed) { train.style.left timePassed / 5 px; }要点Date.now()记录起始时刻每次回调计算已流逝时间timePassed达到总时长后clearInterval结束动画期间每次回调按时间比例更新样式。点击move演示即可看到效果[codetabs height200 srcmove]。使用 requestAnimationFrame让浏览器决定何时重绘setInterval 的两个问题假设同时运行多个动画如果每个动画各自维护一个setInterval(..., 20)那么 20ms 内会有多次相互独立的重绘请求浏览器被迫重复做几何计算与绘制。正确的做法是把多个动画合并进同一个间隔// 更好合并到一次回调中 setInterval(function() { animate1(); animate2(); animate3(); }, 20)// 更差各自独立触发20ms 内多次重绘 setInterval(animate1, 20); setInterval(animate2, 20); setInterval(animate3, 20);第二个问题当 CPU 过载、或浏览器选项卡被隐藏后台标签页时绘图完全没有意义但setInterval依然会持续触发回调、消耗资源。requestAnimationFrame 的语法与行为W3C 的动画时序标准提供了requestAnimationFrame函数它同时解决上述两个问题let requestId requestAnimationFrame(callback);callback会在浏览器每次重绘的最近时间运行通常间隔 10~20ms回调中对元素做的变化会与其他requestAnimationFrame回调和 CSS 动画合并为一次几何计算与重绘页面在后台标签页时根本没有重绘回调不会运行——动画自动暂停不消耗资源返回值requestId用于取消回调cancelAnimationFrame(requestId);callback会收到一个参数——从页面加载开始经过的毫秒数等价于performance.now()的返回值。通常回调很快运行除非 CPU 过载或笔记本电量告急等特殊情况。下面的代码打印前 10 次回调之间的时间间隔读者在浏览器中运行可以看到通常为 10~20msscript let prev performance.now(); let times 0; requestAnimationFrame(function measure(time) { document.body.insertAdjacentHTML(beforeEnd, Math.floor(time - prev) ); prev time; if (times 10) requestAnimationFrame(measure); }); /script仓库还提供了使用requestAnimationFrame重构的小火车演示 move-raf.view/index.html其中内联了一个animate(draw, duration)的早期雏形可对照setInterval版本体会差异function animate(draw, duration) { let start performance.now(); requestAnimationFrame(function animate(time) { let timePassed time - start; if (timePassed duration) timePassed duration; let progress timePassed / duration; // 0 到 1 draw(progress); if (timePassed duration) { requestAnimationFrame(animate); } }); }结构化动画可复用的 animate 框架在requestAnimationFrame基础上文章给出一个更通用的动画函数。它把动画拆成三个正交的维度——时长、时序、绘制从而让“动什么”和“怎么动”完全解耦function animate({timing, draw, duration}) { let start performance.now(); requestAnimationFrame(function animate(time) { // timeFraction 从 0 增加到 1 let timeFraction (time - start) / duration; if (timeFraction 1) timeFraction 1; // 计算当前动画状态 let progress timing(timeFraction); draw(progress); // 绘制 if (timeFraction 1) { requestAnimationFrame(animate); } }); }仓库中的 width.view/animate.js 就是该函数的可运行实现参数顺序写为{duration, draw, timing}语义相同可直接拷贝使用。三个核心参数duration: 动画总时间单位毫秒例如1000。timing(timeFraction): 时序函数类比 CSS 的transition-timing-function。入参是已过去时间与总时间之比0代表开始1代表结束的小数返回动画完成度类似 Bezier 曲线中的y通常也在 0~1 之间。例如线性时序js function linear(timeFraction) { return timeFraction; } 它等价于 transition-timing-function: linear曲线如下 [![linear 时序函数曲线](https://raw.gitcode.com/gh_mirrors/zh/zh.javascript.info/raw/4ae3116347a56fcfed6bbdeb30b76f802c0511a2/7-animation/3-js-animation/linear.svg?utm_sourcegitcode_repo_files)](https://link.gitcode.com/i/2a222f0beac1f9983a02ef0c921952dc)draw(progress): 接收动画完成度progress0为开始状态1为结束状态并执行实际绘制。它可以移动元素js function draw(progress) { train.style.left progress px; } ……也可以做任何其他事情以任何方式为任何事物制作动画。实战把 width 从 0 动画到 100%使用上面的框架让一个进度条元素的宽度从0变化到100%见 width.view/index.html 的完整可运行代码elem.onclick function() { animate({ duration: 1000, timing: function(timeFraction) { return timeFraction; }, draw: function(progress) { elem.style.width progress * 100 %; } }); };与 CSS 动画不同这套方案里时序函数不受 Bezier 曲线限制draw也不局限于修改 CSS 属性——可以为烟花等效果动态创建新元素。时序函数大全n 次幂加速运动想让动画加速可以让progress取n次幂。例如抛物线二次方function quad(timeFraction) { return Math.pow(timeFraction, 2) }三次甚至更大的n会让动画加速得更快。progress为 5 次幂的曲线圆弧function circ(timeFraction) { return 1 - Math.sin(Math.acos(timeFraction)); }反弹弓箭射击“弓箭射击”时序先“拉弓弦”然后“射击”。它依赖附加参数x“弹性系数”定义“拉弓弦”的距离function back(x, timeFraction) { return Math.pow(timeFraction, 2) * ((x 1) * timeFraction - x); }x 1.5时的曲线弹跳想象抛球后落地反弹数次再停下的过程。bounce函数采用相反的顺序弹跳立即开始。它使用了几组特殊系数各系数在for循环中按a b, b / 2迭代决定每一跳的落点与高度function bounce(timeFraction) { for (let a 0, b 1, result; 1; a b, b / 2) { if (timeFraction (7 - 4 * a) / 11) { return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) Math.pow(b, 2) } } }伸缩动画“伸缩”时序附加参数x作为“初始范围”function elastic(x, timeFraction) { return Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(20 * Math.PI * x / 3 * timeFraction) }x 1.5时的曲线以上各时序函数的曲线 SVG 与演示页quad.view、quint.view、circ.view、back.view、bounce.view、elastic.view均位于 7-animation/3-js-animation 目录可在浏览器中直接打开index.html点击体验。逆转时序easeOut 与 easeInOut以上函数直接使用称为easeIn效果在动画开始阶段表现。很多时候需要以相反顺序展示动画即easeOut变换。easeOut数学形式timingEaseOut(timeFraction) 1 - timing(1 - timeFraction)。封装成一个工厂函数// 接受时序函数返回变换后的变体 function makeEaseOut(timing) { return function(timeFraction) { return 1 - timing(1 - timeFraction); } }例如把bounce变成 easeOut 版let bounceEaseOut makeEaseOut(bounce);这样弹跳不在动画开始时执行而是在结束时执行观感更自然。下图展示变换如何改变函数行为——红色为常规弹跳蓝色为 easeOut 弹跳常规弹跳物体在底部弹跳然后突然跳到顶部easeOut变换后物体先跳到顶部然后在顶部弹跳。easeInOut让效果在动画开头和结尾都出现。计算方式if (timeFraction 0.5) { // 动画前半部分 return timing(2 * timeFraction) / 2; } else { // 动画后半部分 return (2 - timing(2 * (1 - timeFraction))) / 2; }封装器function makeEaseInOut(timing) { return function(timeFraction) { if (timeFraction .5) return timing(2 * timeFraction) / 2; else return (2 - timing(2 * (1 - timeFraction))) / 2; } } bounceEaseInOut makeEaseInOut(bounce);“easeInOut” 变换把两段曲线拼在一起前半段是缩小的 “easeIn”后半段是缩小的 “easeOut”动画以相同效果开始和结束。对比circ的三种变体可以看得很清楚——红色为 easeIn常规绿色为 easeOut蓝色为 easeInOut仓库中的 bounce-easeout.view 与 bounce-easeinout.view 是这两种变换的可运行演示配合各自的style.css定义球的落点与场地尺寸。更有趣的 draw不止移动元素只要写出合适的draw就能动画化任何东西。text.view 演示了以弹跳方式逐字落下的文字输入动画——draw在这里操作文本节点而非 CSS 位移。配套练习与源码印证本仓库为该章配了两道实战练习完整答案在对应的solution.view中适合动手验证上文框架为弹跳的球设置动画让球在position:relative的区域内垂直弹跳。核心思路场地底部坐标是field.clientHeight球顶部坐标最底时达到field.clientHeight - ball.clientHeight于是把top从0动画到该差值并配合makeEaseOut(bounce)。最终代码见 solution.mdlet to field.clientHeight - ball.clientHeight; animate({ duration: 2000, timing: makeEaseOut(bounce), draw(progress) { ball.style.top to * progress px } });设置动画使球向右移动在上一题基础上让球同时向右移动100px。水平方向没有“反弹”速度逐渐增加用makeEaseOut(quad)作为时序更自然。两个方向各跑一个animate实例互不干扰见 solution.mdlet height field.clientHeight - ball.clientHeight; let width 100; // 设置 top 动画弹跳 animate({ duration: 2000, timing: makeEaseOut(bounce), draw: function(progress) { ball.style.top height * progress px } }); // 设置 left 动画向右移动 animate({ duration: 2000, timing: makeEaseOut(quad), draw: function(progress) { ball.style.left width * progress px } });这两个练习的完整 HTML 实现含makeEaseOut、bounce、quad内联定义分别在 1-animate-ball/solution.view/index.html 与 2-animate-ball-hops/solution.view/index.html可对照学习。总结JavaScript 动画应通过requestAnimationFrame实现。该内建方法让回调在浏览器准备重绘时运行通常很快但确切时间由浏览器决定页面在后台时没有重绘回调不会运行——动画自动暂停且不消耗资源。推荐的通用 helper 是animatefunction animate({timing, draw, duration}) { let start performance.now(); requestAnimationFrame(function animate(time) { // timeFraction 从 0 增加到 1 let timeFraction (time - start) / duration; if (timeFraction 1) timeFraction 1; // 计算当前动画状态 let progress timing(timeFraction); draw(progress); // 绘制 if (timeFraction 1) { requestAnimationFrame(animate); } }); }三个参数duration为总毫秒数timing接收 0~1 的小数时间、返回 0~1 的进度draw负责实际绘制。JavaScript 动画可以使用任意时序函数幂次、圆弧、反弹、弹跳、伸缩并通过makeEaseOut/makeEaseInOut变换获得更多变体——与 CSS 不同我们不受 Bezier 曲线限制draw同样不受限可以动画化 CSS 之外的任何事物。动画框架本身可以继续改进增加更多能力但 JavaScript 动画通常只用于有趣、不标准的场景建议按需扩展而非过度设计。赞分享文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载相关推荐The JavaScript WayJavaScript 网页动画入门——setInterval、setTimeout 与 requestAnimationFrame 实战The JavaScript WayJavaScript 网页动画入门——setInterval、setTimeout 与 requestAnimationF教程文档Modern JavaScript Tutorial 实战用 requestAnimationFrame 与 easeOut(bounce) 时序实现弹跳球动画Modern JavaScript Tutorial 实战用 requestAnimationFrame 与 easeOut bounce 时序实现弹跳球动画文档/教程前端用 requestAnimationFrame 实现弹跳球动画现代 JavaScript 教程 JS 动画实战解析用 requestAnimationFrame 实现弹跳球动画现代 JavaScript 教程 JS 动画实战解析 本文以《现代 JavaScript 教程》文档/教程前端上一篇RT-Thread APM32F407ZG-EVAL BSP 开发指南从零上手极海 APM32F407 评估板下一篇Sunshine 游戏串流终极指南十分钟免费搭建家庭游戏串流中心创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

Tushare 股东增减持接口(stk_holdertrade)实战指南:股东增减持数据的获取、字段解析与事件驱动应用

Tushare 股东增减持接口(stk_holdertrade)实战指南:股东增减持数据的获取、字段解析与事件驱动应用

金融科技示例工程 【免费下载链接】ai_quant_trade Stock AI Trader: 1-stop platform for learning, sim & live trading. Covers: stock basics, strategies, LLMs, factor mining, ML/DL/RL, graph nets, HFT, C deploy & JoinQuant code. 股票AI操盘手:…

2026/10/9 2:29:36 阅读更多 →
CodePilot Workspace Surface Sidebar:项目 Pin、Primary + Inspector 双栏模型与 v13 迁移实战

CodePilot Workspace Surface Sidebar:项目 Pin、Primary + Inspector 双栏模型与 v13 迁移实战

人工智能AI 应用AI Agent交互助手MCP Clients本地部署 【免费下载链接】CodePilot A multi-model AI agent desktop client — connect any AI provider, extend with MCP & skills, control from your phone. Built with Electron Next.js. 项目地址: https:/…

2026/10/9 2:29:36 阅读更多 →
fframes SVG渲染管线终极揭秘:svgr!宏编译期静态哈希与跨帧缓存如何实现10倍加速

fframes SVG渲染管线终极揭秘:svgr!宏编译期静态哈希与跨帧缓存如何实现10倍加速

fframes SVG渲染管线终极揭秘:svgr!宏编译期静态哈希与跨帧缓存如何实现10倍加速 【免费下载链接】fframes programmatic video rendering framework that is actually fast 项目地址: https://gitcode.com/gh_mirrors/ff/fframes fframes 是一个用 Rust SV…

2026/10/9 2:29:36 阅读更多 →

最新新闻

Agent-Reach 深度解析:Python 构建 CLI 型 AI Agent 的工具调用与避坑指南

Agent-Reach 深度解析:Python 构建 CLI 型 AI Agent 的工具调用与避坑指南

1. 从"Agent-Reach"这个名字说起:它到底想解决什么问题第一次看到"Agent-Reach"这个项目名,我的直觉是:这大概率是一个围绕 AI Agent 能力边界扩展的工具,而不是又一个"套壳聊天机器人"。原因很简单…

2026/10/9 4:02:30 阅读更多 →
Python随机点名器实战:random模块与Tkinter从命令行到GUI

Python随机点名器实战:random模块与Tkinter从命令行到GUI

太好玩了!用Python实现随机点名器,课堂/会议都能用昨天下午最后一节课,我站在讲台上对着花名册喊了三遍“王磊”都没人应,底下一片窃笑——这小子猫在最后一排打盹儿。那一刻我意识到,传统的按花名册顺序点名&#xff…

2026/10/9 4:02:30 阅读更多 →
Agent-Reach CLI工具实战:Python构建AI Agent外部触达能力

Agent-Reach CLI工具实战:Python构建AI Agent外部触达能力

1. 项目缘起与核心定位第一次看到 Agent-Reach 这个标题,我下意识把它拆成了两个部分:Agent 和 Reach。Agent 在当下的技术语境里几乎等同于“能自主干活的智能体”,而 Reach 这个词很有意思,它既可以理解为“触达”,也…

2026/10/9 4:02:30 阅读更多 →
上周最后一天怎么算?Python、SQL、Shell多语言日期计算实战

上周最后一天怎么算?Python、SQL、Shell多语言日期计算实战

你有没有遇到过这样的需求:做报表统计、任务调度或者数据清洗时,经常要算“上周最后一天”。听起来特别简单,不就是减几天的事嘛,可实际动手时,今天周几、系统时区、跨月月初这些因素混在一起,很容易把人绕…

2026/10/9 4:02:30 阅读更多 →
电商数据分析必修课:从数据获取到合规采集的完整指南

电商数据分析必修课:从数据获取到合规采集的完整指南

做电商数据分析这些年,我最深的一个体会是:真正卡住分析进度的往往不是算法模型,而是数据本身。销售报表要出数,运营要复盘,管理层要决策,结果第一步“数据获取”就出各种幺蛾子——要么字段对不上&#xf…

2026/10/9 4:02:30 阅读更多 →
ASP.NET C# ERP源码二次开发:从部署到改造全流程实战

ASP.NET C# ERP源码二次开发:从部署到改造全流程实战

简介:这是一份面向.NET开发团队的ASP.NET C#大型综合管理系统源码包,定位于大型ERP与全能后台管理系统的项目样板,适合具备一定C#基础、希望直接参考完整工程结构或进行二次开发的中高级开发者。压缩包约52.88MB,以zip格式提供&am…

2026/10/9 4:01:29 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 13:34:55 阅读更多 →