在<canvas>中创建动态画面(动画)的核心思路是按照一定时间间隔刷新画布内容,从而实现连续变化的效果。常用的方法有以下几种:
1.requestAnimationFrame(最推荐)
现代浏览器原生支持,专为动画设计的 API。
特点:
- 跟随浏览器刷新率(通常 60fps),节省 CPU/GPU 资源。
- 页面不可见时会自动暂停,降低功耗。
- 语法简单,回调函数中完成“更新状态 → 清屏 → 重绘”流程。
functionanimate(){// 1. 更新动画数据(位置、角度等)// 2. 清空画布 ctx.clearRect(0, 0, width, height)// 3. 重绘所有元素// 4. 继续下一帧requestAnimationFrame(animate);}requestAnimationFrame(animate);2.setInterval/setTimeout(传统但不推荐)
通过定时器触发重绘,可以设定固定帧间隔(如 1000/30 ms)。
缺点:
- 帧率不保真,可能掉帧或过度绘制。
- 即使页面不可见也会执行,浪费资源。
- 与屏幕刷新率不同步,可能产生撕裂或卡顿感。
setInterval(()=>{// 更新状态 → 清屏 → 重绘},1000/30);3. 事件驱动型动画(非循环,但属于动态画面)
通过用户交互或外部事件触发一次性重绘,多次事件累积形成动态效果。
典型场景:
- 鼠标移动时绘制轨迹。
- 滚动页面时更新画布内容。
- 拖动滑块改变图形参数(如颜色、大小)。
canvas.addEventListener('mousemove',(e)=>{// 根据鼠标位置重新绘制drawSomething(e.clientX,e.clientY);});4. 利用setTimeout递归(基本等同于 setInterval,但更灵活)
通过递归调用setTimeout可以实现可调整帧间隔的动画。
可动态改变延迟时间(例如缓动动画时逐渐降低帧率)。
functionanimateWithDelay(delay){// 重绘逻辑setTimeout(()=>animateWithDelay(newDelay),delay);}5. 使用第三方动画库
许多库封装了动画循环,内部依旧使用requestAnimationFrame或 WebGL 的draw方法。
常见例子:
- p5.js:提供
draw()函数自动循环。 - Three.js:通过
requestAnimationFrame驱动renderer.render。 - GSAP(GreenSock):补间引擎,可驱动 canvas 对象的属性变化。
- Fabric.js、Konva.js:内置动画方法(如
animate())。
// p5.js 示例functiondraw(){clear();// 内置清屏ellipse(mouseX,mouseY,50,50);}6. WebGL 专属方法(与 canvas 2D 不同)
如果使用 WebGL(通过canvas.getContext('webgl')),通常也需要一个动画循环。
- 在
requestAnimationFrame中调用gl.clear()和gl.drawArrays()等。 - 也可以借助 WebGL 渲染库(如 Three.js、Babylon.js)的内部更新机制。
7. 离屏渲染 + 增量更新
对于某些只需要局部变化的动画(例如移动一个小球),可以不每次都全屏清空,而是只擦除旧的球位,在新位置绘制。
方法:
- 保存上一帧的状态。
- 用
ctx.clearRect(旧区域)擦除部分内容。 - 绘制新内容。
这种技术常与requestAnimationFrame配合使用,能提高性能。
总结表格
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
requestAnimationFrame | 几乎所有连续动画 | 流畅、省电、同步刷新 | 无法精确控制帧间隔(通常不需要) |
setInterval/setTimeout | 极简单的演示或低帧率需求 | 实现简单 | 不流畅、页面不可见时浪费资源 |
| 事件驱动 | 交互式动态画面(非自动动画) | 节省计算资源 | 不适合自动运动 |
| 第三方库(p5/Three/GSAP) | 复杂动画或3D场景 | 功能丰富、跨平台 | 额外依赖体积 |
| 离屏 + 局部擦除 | 需要高性能的局部移动 | 减少绘制面积 | 实现复杂,需管理脏矩形 |
最佳实践:除非有特殊原因(如精确间隔控制),始终默认使用requestAnimationFrame作为 canvas 动画的主循环。
requestAnimationFrame详解
requestAnimationFrame是浏览器专门为动画设计的 API,用于在下次重绘之前调用指定的回调函数,从而实现流畅、省电、高性能的动画效果。
一、一句话概括
告诉浏览器:你下一次要重绘屏幕时,请先执行我给你的这个函数。
浏览器通常每秒重绘 60 次(即 60fps,每帧约 16.6ms)。requestAnimationFrame会在每次重绘前调用你的回调,你可以在回调中更新动画状态并重新绘制 canvas/DOM。
二、为什么需要它?—— 与setTimeout/setInterval的对比
| 特性 | requestAnimationFrame | setTimeout/setInterval |
|---|---|---|
| 执行时机 | 下次浏览器重绘之前(自动与刷新率同步) | 指定的毫秒后(与重绘周期无关) |
| 帧率 | 匹配设备刷新率(通常 60fps) | 手动指定间隔,可能造成丢帧或过绘制 |
| 页面不可见时 | 自动暂停,不执行回调 | 依然执行,浪费 CPU/电池 |
| 精度 | 高(由浏览器调度) | 受事件循环影响,不精确(嵌套延迟最小 4ms) |
| 性能 | 优秀,与渲染管线集成 | 容易产生卡顿、撕裂 |
| 适用场景 | 动画、canvas 重绘、滚动监听优化 | 定时任务、非动画循环 |
核心差异:setTimeout只是把任务放到任务队列,不关心浏览器是否准备重绘。requestAnimationFrame则与渲染流水线绑定,在每一帧开始时执行,保证更新和绘制同步。
三、基本用法
functionanimate(){// 1. 更新动画状态(例如改变位置、角度、透明度)// 2. 重绘画布(ctx.clearRect 后重新绘制所有图形)// 3. 请求下一帧requestAnimationFrame(animate);}// 启动动画requestAnimationFrame(animate);重要:必须在回调函数的末尾再次调用requestAnimationFrame,否则动画只会执行一帧就停止。
四、回调函数接收的参数
回调函数会被传入一个高精度时间戳(DOMHighResTimeStamp),表示从页面加载开始到当前帧触发的时间(毫秒)。可以利用这个时间戳实现与帧率无关的动画:
letstartTime=null;functionanimate(timestamp){if(!startTime)startTime=timestamp;constelapsed=timestamp-startTime;// 经过的毫秒数// 根据时间决定物体的位置,而不是每帧固定移动距离constx=(elapsed*0.1)%500;// 这样无论帧率是 30 还是 60,物体移动速度恒定requestAnimationFrame(animate);}五、取消动画:cancelAnimationFrame
与setTimeout对应,requestAnimationFrame会返回一个非零整数 ID,可以用它取消:
letrafId=requestAnimationFrame(animate);// 当不再需要动画时cancelAnimationFrame(rafId);六、工作流程与浏览器渲染管线
一次典型的“帧”包含以下步骤:
- JavaScript 执行(
requestAnimationFrame回调在此阶段执行)
→ 修改 DOM / canvas / CSS 样式。 - 样式计算(Recalculate Style)
- 布局(Layout / Reflow)
- 绘制(Paint)
- 合成(Composite)
requestAnimationFrame保证回调在1和2 之间执行,这样修改样式后能立即在同一帧中被布局和绘制,避免多帧延迟。
七、控制帧率(节流)
有时不需要以 60fps 渲染(如简单动画、数据图表),为了节省 CPU,可以手动节流:
letlastTimestamp=0;constinterval=1000/30;// 目标 30fpsfunctionanimate(timestamp){if(timestamp-lastTimestamp>=interval){// 执行更新和重绘update();draw();lastTimestamp=timestamp;}requestAnimationFrame(animate);}注意:这种节流只是跳过了部分帧的重绘,requestAnimationFrame本身仍在以 60fps 被调用。
八、在 canvas 动画中的典型模式
constcanvas=document.getElementById('canvas');constctx=canvas.getContext('2d');letx=0,y=100;letspeed=2;functiondraw(){// 清空画布ctx.clearRect(0,0,canvas.width,canvas.height);// 绘制物体ctx.fillStyle='red';ctx.fillRect(x,y,50,50);}functionupdate(){x+=speed;if(x>canvas.width+50)x=-50;}functionanimate(){update();// 更新逻辑(与帧率无关的状态改变最好用时间差)draw();// 重绘requestAnimationFrame(animate);}animate();九、常见误区与最佳实践
不要在回调中进行过重的同步计算
如果每帧处理数据超过 16ms,就会发生掉帧(skipping frames)。应该把复杂计算拆分到 Web Worker 或优化算法。配合
ctx.clearRect使用
如果不清屏,旧图像会残留,造成拖影。除非你刻意要“残影”效果。优先使用相对于时间的位移,而非帧计数
否则帧率变化会导致速度变化。正确做法:记录上一帧的时间戳,计算时间差deltaTime,然后position += speed * deltaTime。letlastTime=0;functionanimate(now){letdelta=Math.min(100,now-lastTime);// 限制最大 delta 避免跳跃过大lastTime=now;position+=speed*delta;requestAnimationFrame(animate);}避免在回调中读取强制同步布局的属性
如offsetTop、clientWidth、getComputedStyle等,会触发强制重排,破坏渲染管线,导致卡顿。页面隐藏时停止动画
requestAnimationFrame已经会自动暂停,无需额外处理。但如果你想在页面隐藏时完全停止某些音频或网络请求,可以监听visibilitychange事件。兼容旧浏览器
IE10+ 才支持,更老的需要降级到setTimeout。现在的项目基本可忽略。
十、性能监控(fps 计算)
你可以通过requestAnimationFrame简单计算实时帧率:
letframeCount=0;letlastSec=performance.now();functionmeasureFPS(now){frameCount++;if(now-lastSec>=1000){console.log('FPS:',frameCount);frameCount=0;lastSec=now;}requestAnimationFrame(measureFPS);}十一、与其他浏览器的渲染 API 协同
Canvas 2D:最常用,直接在回调中清屏重绘。WebGL:同样在回调中调用gl.drawArrays/gl.drawElements。SVG/DOM属性动画:也可以在requestAnimationFrame中修改元素样式或属性,利用浏览器合成器,无需手动重绘。- 配合
IntersectionObserver:当元素进入视口时才启动动画循环,节省资源。
十二、总结
| 方面 | 结论 |
|---|---|
| 何时用 | 任何需要连续、流畅的视觉变化(canvas 动画、滚动视差、JS 驱动的 DOM 动效) |
| 何时不用 | 一帧性的绘制(页面加载时画一个静态图)、与时间间隔严格无关的定时任务 |
| 核心优势 | 帧同步、省电、高性能、自动适配刷新率 |
| 核心缺点 | 无法像setTimeout那样精确指定毫秒延迟(但动画本来也不需要) |
记住一句话:
在 requestAnimationFrame 出现之前,网页动画全靠 setTimeout 猜帧;之后,动画终于和显示器“握手”了。
如果你有关于requestAnimationFrame与其他 API 组合使用的具体场景(如粒子系统、游戏循环、图表重绘),欢迎继续提问!