useFrame 详解
🎯 核心功能
useFrame 是 @react-three/fiber 提供的 Hook,用于在每一帧渲染时执行回调函数。
📖 实现原理
简化版源码逻辑
// @react-three/fiber 内部实现(简化版)
function useFrame(callback: (state: RootState, delta: number) => void, priority?: number) {
const { subscribe } = useThree(); // 获取渲染循环
useEffect(() => {
// 1️⃣ 订阅渲染循环
const unsubscribe = subscribe(callback, priority);
// 2️⃣ 组件卸载时取消订阅
return () => unsubscribe();
}, [callback, subscribe, priority]);
}
// Three.js 渲染循环
function animate() {
requestAnimationFrame(animate); // 递归调用,通常 60fps
const delta = clock.getDelta(); // 距离上一帧的时间(秒)
// 执行所有订阅的 useFrame 回调
subscribers.forEach(callback => {
callback(state, delta);
});
renderer.render(scene, camera); // 渲染场景
}🔧 使用方式
基础用法
useFrame((state, delta) => {
// 每帧执行 (60fps = 每秒执行 60 次)
console.log('帧间隔时间:', delta); // 约 0.016 秒
console.log('当前相机:', state.camera);
});完整参数说明
useFrame(
(state, delta, xrFrame) => {
// state: 包含 scene, camera, gl, size, viewport 等
// delta: 距离上一帧的时间(秒)
// xrFrame: WebXR 相关(VR/AR 开发使用)
},
1 // priority: 执行优先级(数字越小越先执行)
);🎮 实际应用场景
1️⃣ 平滑动画
useFrame((state, delta) => {
// 旋转立方体
meshRef.current.rotation.y += delta * 0.5; // 每秒旋转 0.5 弧度
});2️⃣ 相机跟随
useFrame(({ camera }) => {
// 相机平滑跟随玩家
camera.position.lerp(
new THREE.Vector3(
player.position.x,
player.position.y + 5,
player.position.z + 10
),
0.1 // 插值系数,越小越平滑
);
camera.lookAt(player.position);
});3️⃣ 物理更新
useFrame(() => {
const hasMovement = forward || backward || left || right;
// 每帧计算速度
velocity
.set(Number(right) - Number(left), 0, Number(backward) - Number(forward))
.normalize()
.multiplyScalar(SPEED);
// 应用到刚体
player.current.setLinvel(velocity, true);
});📊 性能对比:useFrame vs 之前的方案
❌ 之前的方案(useKeyboardControls 回调)
useKeyboardControls((state) => {
move(state);
return Object.values(state).some(Boolean);
});问题:
✅ 使用 useFrame 后
const forward = useKeyboardControls((state) => state.forward);
const backward = useKeyboardControls((state) => state.backward);
const left = useKeyboardControls((state) => state.left);
const right = useKeyboardControls((state) => state.right);
useFrame(() => {
const hasMovement = forward || backward || left || right;
// 每帧执行逻辑
player.current.setLinvel(velocity, true);
});优势:
🔬 深入对比
执行频率
// ❌ 旧方案:键盘事件触发
useKeyboardControls((state) => {
console.log('触发次数: 不确定,取决于按键频率');
// 按住按键可能每秒触发 10-30 次(操作系统决定)
});
// ✅ 新方案:每帧触发
useFrame(() => {
console.log('触发次数: 60 次/秒 (稳定)');
// 与显示器刷新率同步
});内存使用
// ❌ 旧方案:频繁触发组件渲染
每次 setStatus() → React 重新渲染 → 创建新的虚拟 DOM → 对比差异
// 假设每秒触发 20 次 setStatus,每次渲染耗时 5ms
// = 每秒浪费 100ms 在渲染上
// ✅ 新方案:只在状态真正变化时渲染
useFrame(() => {
if (status !== newStatus) { // 只有变化时才渲染
setStatus(newStatus);
}
});
// 假设每秒只改变 2 次状态
// = 每秒只渲染 2 次,节省 90% 性能🎯 完整优化对比表
💡 最佳实践建议
✅ 推荐:useFrame + 订阅式状态
// 订阅键盘状态(不触发渲染)
const forward = useKeyboardControls((state) => state.forward);
// 每帧更新逻辑
useFrame(() => {
if (forward) {
// 移动逻辑
}
});❌ 避免:在 useKeyboardControls 回调中修改 state
// 会导致无限渲染
useKeyboardControls((state) => {
setStatus('run'); // ❌ 错误
return state.forward;
});⚠️ 注意:useFrame 的清理
useFrame(() => {
// 确保引用存在
if (!player.current) return;
// 执行逻辑
player.current.setLinvel(velocity, true);
});
// useFrame 会在组件卸载时自动清理,无需手动取消订阅🚀 性能监控对比
// 添加性能监控
useFrame((state, delta) => {
const start = performance.now();
// 你的逻辑
player.current.setLinvel(velocity, true);
const end = performance.now();
console.log(`帧耗时: ${end - start}ms`);
console.log(`帧间隔: ${delta * 1000}ms (应该约为 16.67ms)`);
});预期结果:
旧方案:帧耗时 5-15ms,帧间隔不稳定
新方案:帧耗时 0.5-2ms,帧间隔稳定在 16.67ms (60fps)
🎓 总结
useFrame 是 React Three Fiber 中处理实时更新的标准方案,特别适合:
✅ 物理模拟
✅ 动画控制
✅ 相机跟随
✅ 性能关键路径
通过这个优化,从事件驱动转变为帧驱动,性能和流畅度都有质的飞跃!🚀
useFrame 是 React Three Fiber 的语法糖
是的!useFrame 是 @react-three/fiber 提供的语法糖,底层就是 Three.js 的渲染循环。
🔧 Three.js 原生实现
方式 1: 使用 requestAnimationFrame (最常见)
import * as THREE from 'three';
// 创建场景、相机、渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建一个立方体
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
// 📌 这就是 useFrame 的底层实现
const clock = new THREE.Clock();
function animate() {
// 🔁 递归调用,创建渲染循环
requestAnimationFrame(animate);
// ⏱️ 获取 delta 时间(类似 useFrame 的第二个参数)
const delta = clock.getDelta();
// 🎮 每帧更新逻辑(类似 useFrame 的回调内容)
cube.rotation.x += delta;
cube.rotation.y += delta * 0.5;
// 🎨 渲染场景
renderer.render(scene, camera);
}
// 🚀 启动渲染循环
animate();方式 2: 使用 Three.js 的 Clock 和 setAnimationLoop
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer();
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight);
const cube = new THREE.Mesh(
new THREE.BoxGeometry(),
new THREE.MeshBasicMaterial({ color: 0x00ff00 })
);
scene.add(cube);
const clock = new THREE.Clock();
// 📌 使用 setAnimationLoop(推荐用于 WebXR)
renderer.setAnimationLoop((time) => {
const delta = clock.getDelta();
// 每帧更新
cube.rotation.y += delta;
renderer.render(scene, camera);
});🎯 用 Three.js 原生实现
React Three Fiber 版本
useFrame(() => {
if (!camera || !player.current) return;
const hasMovement = forward || backward || left || right;
const newStatus = hasMovement ? names[STATUS.run] : names[STATUS.idle];
if (status !== newStatus) {
setStatus(newStatus);
}
velocity
.set(Number(right) - Number(left), 0, Number(backward) - Number(forward))
.normalize()
.multiplyScalar(SPEED)
.applyEuler(camera.rotation);
player.current.setLinvel({
x: toFixed(velocity.x),
y: player.current.linvel().y,
z: toFixed(velocity.z),
}, true);
});Three.js 原生版本(等价实现)
import * as THREE from 'three';
import * as RAPIER from '@dimforge/rapier3d-compat';
// 场景设置
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight);
const renderer = new THREE.WebGLRenderer();
const clock = new THREE.Clock();
// 键盘状态
const keys = {
forward: false,
backward: false,
left: false,
right: false,
jump: false,
};
window.addEventListener('keydown', (e) => {
if (e.key === 'w') keys.forward = true;
if (e.key === 's') keys.backward = true;
if (e.key === 'a') keys.left = true;
if (e.key === 'd') keys.right = true;
if (e.key === ' ') keys.jump = true;
});
window.addEventListener('keyup', (e) => {
if (e.key === 'w') keys.forward = false;
if (e.key === 's') keys.backward = false;
if (e.key === 'a') keys.left = false;
if (e.key === 'd') keys.right = false;
if (e.key === ' ') keys.jump = false;
});
// 物理世界
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 });
// 玩家刚体
const playerBody = world.createRigidBody(
RAPIER.RigidBodyDesc.dynamic()
.setTranslation(0, 1, 0)
.lockRotations()
);
// 胶囊碰撞器
const colliderDesc = RAPIER.ColliderDesc.capsule(0.6, 0.3);
world.createCollider(colliderDesc, playerBody);
// 玩家模型(假设已加载)
let playerModel;
// 使用 GLTFLoader 加载模型...
// 变量
const SPEED = 4;
const JUMP = 7;
const velocity = new THREE.Vector3();
let isGrounded = true;
// 📌 渲染循环(等价于 useFrame)
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
// ✅ 更新物理世界
world.step();
// ✅ 判断动画状态
const hasMovement = keys.forward || keys.backward || keys.left || keys.right;
const newStatus = hasMovement ? 'run' : 'idle';
// 播放动画(需要动画混合器)
if (currentStatus !== newStatus) {
currentStatus = newStatus;
// mixer.clipAction(animations[newStatus]).play();
}
// ✅ 计算移动方向
velocity.set(
Number(keys.right) - Number(keys.left),
0,
Number(keys.backward) - Number(keys.forward)
)
.normalize()
.multiplyScalar(SPEED)
.applyEuler(camera.rotation);
// ✅ 获取当前速度
const currentVel = playerBody.linvel();
// ✅ 设置新速度
playerBody.setLinvel({
x: velocity.x,
y: currentVel.y,
z: velocity.z,
}, true);
// ✅ 跳跃
if (keys.jump && isGrounded) {
playerBody.setLinvel({
x: velocity.x,
y: JUMP,
z: velocity.z,
}, true);
isGrounded = false;
}
// ✅ 同步模型位置到刚体
if (playerModel) {
const pos = playerBody.translation();
playerModel.position.set(pos.x, pos.y, pos.z);
const rot = playerBody.rotation();
playerModel.quaternion.set(rot.x, rot.y, rot.z, rot.w);
}
// ✅ 渲染
renderer.render(scene, camera);
}
// 🚀 启动
animate();📊 对比分析
🎓 useFrame 的核心价值
1️⃣ 自动管理生命周期
// ✅ React Three Fiber
function MyComponent() {
useFrame(() => {
// 逻辑
});
// 组件卸载时自动清理 ✅
}
// ❌ Three.js 原生
let animationId;
function animate() {
animationId = requestAnimationFrame(animate);
// 逻辑
}
animate();
// 需要手动清理 ❌
function cleanup() {
cancelAnimationFrame(animationId);
}2️⃣ 与 React 状态同步
// ✅ 可以直接访问 React 状态
const [position, setPosition] = useState([0, 0, 0]);
useFrame(() => {
setPosition([x, y, z]); // 自动重新渲染
});
// ❌ 原生需要手动同步
let position = [0, 0, 0];
function animate() {
position = [x, y, z];
// 需要手动触发 UI 更新
updateUI(position);
}3️⃣ 优先级控制
// 高优先级(先执行)
useFrame(() => {
// 物理更新
}, 1);
// 低优先级(后执行)
useFrame(() => {
// 渲染后处理
}, 10);🔍 useFrame 源码简化版
// @react-three/fiber/src/core/hooks.tsx
export function useFrame(
callback: (state: RootState, delta: number) => void,
priority: number = 0
): void {
const { subscribe } = useThree();
useEffect(() => {
// 订阅渲染循环
const unsubscribe = subscribe(callback, priority);
// 组件卸载时取消订阅
return () => unsubscribe();
}, [callback, priority, subscribe]);
}
// 渲染循环管理器
class RenderLoop {
private subscribers: Array<{
callback: Function;
priority: number;
}> = [];
subscribe(callback: Function, priority: number) {
this.subscribers.push({ callback, priority });
this.subscribers.sort((a, b) => a.priority - b.priority);
return () => {
const index = this.subscribers.findIndex(s => s.callback === callback);
this.subscribers.splice(index, 1);
};
}
loop() {
requestAnimationFrame(() => this.loop());
const delta = this.clock.getDelta();
// 按优先级执行所有订阅
this.subscribers.forEach(({ callback }) => {
callback(this.state, delta);
});
this.renderer.render(this.scene, this.camera);
}
}💡 总结
useFrame 本质上是对 Three.js requestAnimationFrame 渲染循环的 React 封装,提供了:
✅ 自动生命周期管理
✅ 与 React 状态同步
✅ 优先级控制
✅ 更简洁的 API
对于 React 项目,强烈建议使用 useFrame,而不是自己写原生循环!