代码: 全选
在 JavaScript 中使用**函數遞歸**來控制動畫,最常見且推薦的方式是搭配 **`requestAnimationFrame`** 來實現,而不是用純粹的同步遞歸(因為同步遞歸會造成呼叫堆疊爆炸)。
以下是幾種常見的寫法,從簡單到實用,逐步說明:
### 1. 最經典寫法(推薦大多數情況使用)
```javascript
let isRunning = false;
function animate() {
// 這裡放每一幀要執行的內容
console.log("正在動畫中...");
// 畫面更新邏輯,例如:
// box.style.transform = `translateX(${progress}px)`;
if (isRunning) {
requestAnimationFrame(animate); // 自己呼叫自己 → 形成遞歸
}
}
// 啟動動畫
function startAnimation() {
if (!isRunning) {
isRunning = true;
requestAnimationFrame(animate);
}
}
// 停止動畫
function stopAnimation() {
isRunning = false;
}
// 使用範例
startAnimation();
// 3秒後停止
setTimeout(stopAnimation, 3000);
```
這是最常見的「**自我遞歸 + 旗標控制**」寫法。
### 2. 帶時間控制 & 進度計算(最推薦實戰寫法)
```javascript
let startTime = null;
const duration = 2000; // 動畫持續 2 秒
function animate(timestamp) {
// 第一次執行時記錄開始時間
if (!startTime) startTime = timestamp;
// 計算已經經過的時間比例 (0 ~ 1)
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1); // 不超過1
// 套用緩動函數(可選)
// const eased = progress ** 2; // 二次緩入
// const eased = 1 - (1 - progress) ** 3; // 強緩出
const eased = progress; // 線性
// 實際更新畫面
const moveDistance = 300 * eased;
box.style.transform = `translateX(${moveDistance}px)`;
// 還沒結束就繼續下一幀
if (progress < 1) {
requestAnimationFrame(animate);
} else {
console.log("動畫結束");
// 可在此呼叫結束回調
}
}
// 啟動
requestAnimationFrame(animate);
```
### 3. 更乾淨的類別封裝寫法(推薦中大型專案)
```javascript
class Animator {
constructor(element) {
this.element = element;
this.isRunning = false;
this.startTime = null;
this.duration = 1500;
this.from = 0;
this.to = 300;
}
// 緩動函數可自行替換
ease(t) {
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
}
animate = (timestamp) => {
if (!this.startTime) this.startTime = timestamp;
const t = (timestamp - this.startTime) / this.duration;
const progress = Math.min(t, 1);
const eased = this.ease(progress);
const value = this.from + (this.to - this.from) * eased;
this.element.style.transform = `translateX(${value}px)`;
if (progress < 1 && this.isRunning) {
requestAnimationFrame(this.animate);
} else if (progress >= 1) {
console.log("動畫完成");
}
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.startTime = null;
requestAnimationFrame(this.animate);
}
stop() {
this.isRunning = false;
}
reset() {
this.stop();
this.element.style.transform = `translateX(${this.from}px)`;
}
}
// 使用
const box = document.querySelector('.box');
const animator = new Animator(box);
animator.start();
// 想停止就呼叫
// animator.stop();
```
### 4. 什麼時候才會真的用「純遞歸」而不搭配 rAF?
極少數特殊場景,例如:
- 一次性的階層式展開/收合動畫(樹狀菜單、accordion)
- 逐步顯示的文字逐字動畫(但通常也會搭配 setTimeout)
範例(逐字顯示):
```javascript
function typeText(text, element, speed = 80, i = 0) {
if (i < text.length) {
element.textContent += text[i];
setTimeout(() => {
typeText(text, element, speed, i + 1);
}, speed);
}
}
typeText("Hello! 這是用遞歸實現的打字機效果~", document.querySelector('.text'));
```
### 總結:常見控制動畫的方式對比
| 方式 | 是否推薦 | 優點 | 缺點 | 使用場景 |
|-----------------------|--------------|------------------------------------|-----------------------------|---------------------------|
| requestAnimationFrame + 遞歸 | ★★★★★ | 流暢、省電、跟螢幕刷新率同步 | 需要手動控制開始/停止 | 幾乎所有自訂 JS 動畫 |
| setInterval | ★★☆☆☆ | 簡單 | 不跟螢幕同步、容易掉幀 | 簡單計時器、非視覺動畫 |
| setTimeout 遞歸 | ★★★☆☆ | 容易控制間隔 | 不夠平滑、耗能較高 | 逐字、逐行、延遲觸發 |
| CSS transition/animation | ★★★★★ | 效能最好、硬體加速 | 控制彈性較低 | 一般 UI 動畫 |
| Web Animations API | ★★★★☆ | 原生、精確控制 | 瀏覽器支援度仍稍差 | 需要精確回調的高級動畫 |
**最推薦的日常寫法就是第2或第3種**(帶時間計算的 rAF 遞歸)。
有沒有具體想做的動畫效果?
例如:位移、淡入淡出、旋轉、畫布粒子、SVG 描邊、打字機、拋物線……我可以直接給你對應的完整範例程式碼。