事件系统
Three.js 的事件系统包括渲染器 DOM 事件、场景事件和自定义事件,通过事件驱动实现复杂的交互逻辑。
概述
Three.js 提供了完整的事件系统,支持鼠标、键盘、触摸、窗口等各种事件,以及基于 EventDispatcher 的自定义事件机制。本文档详细介绍各类事件的使用方法和最佳实践。
事件系统架构
plaintext
┌─────────────────────────────────────────────────────────────┐
│ Three.js 事件系统 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ DOM 事件 │ │ Object3D 事件 │ │ 自定义事件 │ │
│ ├─────────────────┤ ├─────────────────┤ ├─────────────┤ │
│ │ • mousemove │ │ • added │ │ • EventDisp │ │
│ │ • mousedown/up │ │ • removed │ │ atcher │ │
│ │ • click │ │ • change │ │ • 自定义类型 │ │
│ │ • wheel │ │ • dispose │ │ │ │
│ │ • keydown/up │ │ │ │ │ │
│ │ • touchstart │ │ │ │ │ │
│ │ • touchmove │ │ │ │ │ │
│ │ • resize │ │ │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
│ │ │ │ │
│ └───────────────────┴───────────────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ 事件管理器/分发器 │ │
│ └───────────────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ 回调函数/处理逻辑 │ │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────────────┘事件类型对比
| 类型 | 来源 | 用途 | 示例 |
|---|---|---|---|
| DOM 事件 | 浏览器 | 用户输入处理 | 鼠标点击、键盘按键 |
| Object3D 事件 | Three.js 内部 | 对象状态变化 | 添加到场景、移除 |
| 自定义事件 | 开发者定义 | 业务逻辑通信 | 对象选中、动画完成 |
渲染器 DOM 事件
鼠标事件
javascript
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer();
document.body.appendChild(renderer.domElement);
// 鼠标按下
renderer.domElement.addEventListener('mousedown', (event) => {
console.log('鼠标按下:', event.button);
// event.button: 0=左键, 1=中键, 2=右键
});
// 鼠标释放
renderer.domElement.addEventListener('mouseup', (event) => {
console.log('鼠标释放:', event.button);
});
// 鼠标移动
renderer.domElement.addEventListener('mousemove', (event) => {
console.log('鼠标移动:', event.clientX, event.clientY);
});
// 鼠标点击(完整点击序列:mousedown → mouseup → click)
renderer.domElement.addEventListener('click', (event) => {
console.log('鼠标点击');
});
// 双击
renderer.domElement.addEventListener('dblclick', (event) => {
console.log('双击');
});
// 鼠标进入画布
renderer.domElement.addEventListener('mouseenter', (event) => {
console.log('鼠标进入');
});
// 鼠标离开画布
renderer.domElement.addEventListener('mouseleave', (event) => {
console.log('鼠标离开');
});
// 滚轮
renderer.domElement.addEventListener('wheel', (event) => {
console.log('滚轮:', event.deltaY);
event.preventDefault(); // 阻止页面滚动
});
// 上下文菜单(右键菜单)
renderer.domElement.addEventListener('contextmenu', (event) => {
event.preventDefault(); // 阻止默认菜单
console.log('右键菜单');
});鼠标事件对象属性
| 属性 | 类型 | 说明 |
|---|---|---|
clientX/Y | number | 相对于视口的坐标 |
pageX/Y | number | 相对于页面的坐标(含滚动) |
offsetX/Y | number | 相对于目标元素的坐标 |
movementX/Y | number | 相对于上一次移动的偏移 |
button | number | 按下的按钮(0=左, 1=中, 2=右) |
buttons | number | 当前按下的按钮组合(位掩码) |
shiftKey | boolean | Shift 是否按下 |
ctrlKey | boolean | Ctrl 是否按下 |
altKey | boolean | Alt 是否按下 |
metaKey | boolean | Meta/Command 是否按下 |
键盘事件
javascript
// 键盘按下
window.addEventListener('keydown', (event) => {
console.log('按键:', event.key, event.code, event.keyCode);
// 常用按键检测
if (event.key === 'w' || event.key === 'W') {
console.log('按下 W 键');
}
if (event.key === 'ArrowUp') {
console.log('按下上箭头');
}
// 组合键
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
console.log('Ctrl+S');
}
if (event.shiftKey) {
console.log('按住 Shift');
}
if (event.altKey && event.key === 'Enter') {
console.log('Alt+Enter');
}
});
// 键盘释放
window.addEventListener('keyup', (event) => {
console.log('释放:', event.key);
});按键持续检测
javascript
// 使用按键状态对象
const keys = {};
window.addEventListener('keydown', (event) => {
keys[event.code] = true;
});
window.addEventListener('keyup', (event) => {
keys[event.code] = false;
});
// 在动画循环中检测
function animate() {
requestAnimationFrame(animate);
const speed = 0.1;
if (keys['KeyW'] || keys['ArrowUp']) {
camera.position.z -= speed;
}
if (keys['KeyS'] || keys['ArrowDown']) {
camera.position.z += speed;
}
if (keys['KeyA'] || keys['ArrowLeft']) {
camera.position.x -= speed;
}
if (keys['KeyD'] || keys['ArrowRight']) {
camera.position.x += speed;
}
if (keys['Space']) {
camera.position.y += speed;
}
if (keys['ShiftLeft'] || keys['ShiftRight']) {
camera.position.y -= speed;
}
renderer.render(scene, camera);
}
animate();键盘事件属性
| 属性 | 类型 | 说明 |
|---|---|---|
key | string | 按键值(如 'a', 'Enter', 'ArrowUp') |
code | string | 物理按键代码(如 'KeyA', 'Enter', 'ArrowUp') |
keyCode | number | 已废弃,建议使用 key 或 code |
repeat | boolean | 是否为重复按键(长按) |
触摸事件
javascript
// 触摸开始
renderer.domElement.addEventListener('touchstart', (event) => {
event.preventDefault(); // 阻止默认行为(如滚动)
const touches = event.touches;
console.log(`触摸开始: ${touches.length} 个触摸点`);
for (let i = 0; i < touches.length; i++) {
console.log(`触摸点 ${i}:`, touches[i].clientX, touches[i].clientY);
}
});
// 触摸移动
renderer.domElement.addEventListener('touchmove', (event) => {
event.preventDefault(); // 阻止页面滚动
const touches = event.touches;
console.log('触摸移动:', touches.length, '个触摸点');
});
// 触摸结束
renderer.domElement.addEventListener('touchend', (event) => {
console.log('触摸结束');
console.log('剩余触摸点:', event.touches.length);
});
// 触摸取消(来电、系统弹窗等)
renderer.domElement.addEventListener('touchcancel', (event) => {
console.log('触摸取消');
});触摸事件对象
javascript
renderer.domElement.addEventListener('touchstart', (event) => {
// touches: 当前屏幕上的所有触摸点
const touches = event.touches;
// targetTouches: 当前元素上的所有触摸点
const targetTouches = event.targetTouches;
// changedTouches: 触发此次事件的触摸点
const changedTouches = event.changedTouches;
// 单个触摸点信息
const touch = touches[0];
console.log({
identifier: touch.identifier, // 唯一标识
clientX: touch.clientX, // 视口 X 坐标
clientY: touch.clientY, // 视口 Y 坐标
pageX: touch.pageX, // 页面 X 坐标
pageY: touch.pageY, // 页面 Y 坐标
screenX: touch.screenX, // 屏幕 X 坐标
screenY: touch.screenY // 屏幕 Y 坐标
});
});多点触控示例
javascript
// 缩放示例(双指缩放)
let lastTouchDistance = 0;
renderer.domElement.addEventListener('touchmove', (event) => {
if (event.touches.length === 2) {
// 计算两个触摸点之间的距离
const dx = event.touches[0].clientX - event.touches[1].clientX;
const dy = event.touches[0].clientY - event.touches[1].clientY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (lastTouchDistance > 0) {
// 计算缩放比例
const scale = distance / lastTouchDistance;
// 应用缩放(示例:缩放相机距离)
camera.position.multiplyScalar(1 / scale);
}
lastTouchDistance = distance;
}
});
renderer.domElement.addEventListener('touchend', () => {
lastTouchDistance = 0;
});
// 旋转示例(双指旋转)
let lastAngle = 0;
renderer.domElement.addEventListener('touchmove', (event) => {
if (event.touches.length === 2) {
const angle = Math.atan2(
event.touches[1].clientY - event.touches[0].clientY,
event.touches[1].clientX - event.touches[0].clientX
);
if (lastAngle !== 0) {
const deltaAngle = angle - lastAngle;
// 应用旋转
scene.rotation.y += deltaAngle;
}
lastAngle = angle;
}
});
renderer.domElement.addEventListener('touchend', () => {
lastAngle = 0;
});
// 拖拽示例(单指拖拽)
let lastTouchX = 0;
let lastTouchY = 0;
renderer.domElement.addEventListener('touchstart', (event) => {
if (event.touches.length === 1) {
lastTouchX = event.touches[0].clientX;
lastTouchY = event.touches[0].clientY;
}
});
renderer.domElement.addEventListener('touchmove', (event) => {
if (event.touches.length === 1) {
const deltaX = event.touches[0].clientX - lastTouchX;
const deltaY = event.touches[0].clientY - lastTouchY;
// 应用移动
camera.position.x -= deltaX * 0.01;
camera.position.y += deltaY * 0.01;
lastTouchX = event.touches[0].clientX;
lastTouchY = event.touches[0].clientY;
}
});窗口事件
javascript
// 窗口大小改变
window.addEventListener('resize', (event) => {
const width = window.innerWidth;
const height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
// 设备方向变化(移动端)
window.addEventListener('orientationchange', () => {
console.log('屏幕方向:', screen.orientation.type);
});
// 窗口获得焦点
window.addEventListener('focus', () => {
console.log('窗口获得焦点');
// 可用于恢复动画
});
// 窗口失去焦点
window.addEventListener('blur', () => {
console.log('窗口失去焦点');
// 可用于暂停动画
});
// 页面可见性改变
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('页面隐藏');
// 暂停动画、释放资源
} else {
console.log('页面显示');
// 恢复动画
}
});
// 页面卸载前
window.addEventListener('beforeunload', (event) => {
// 保存状态、清理资源
console.log('页面即将卸载');
});
// 设备像素比变化(高分屏切换)
window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
.addEventListener('change', () => {
renderer.setPixelRatio(window.devicePixelRatio);
});Object3D 事件
Three.js 的 Object3D 使用 EventDispatcher 提供事件系统。
内置事件
javascript
const mesh = new THREE.Mesh();
// 监听添加到场景事件
mesh.addEventListener('added', (event) => {
console.log('对象添加到场景', event.target);
});
// 监听从场景移除事件
mesh.addEventListener('removed', (event) => {
console.log('对象从场景移除', event.target);
});
// 添加到场景(触发 'added' 事件)
scene.add(mesh);
// 从场景移除(触发 'removed' 事件)
scene.remove(mesh);自定义事件
javascript
const mesh = new THREE.Mesh();
// 监听自定义事件
mesh.addEventListener('selected', (event) => {
console.log('对象被选中:', event.message);
console.log('选中坐标:', event.point);
});
mesh.addEventListener('deselected', (event) => {
console.log('对象取消选中');
});
// 派发自定义事件
function selectObject(mesh, point) {
mesh.dispatchEvent({
type: 'selected',
message: 'User selected this object',
point: point,
timestamp: Date.now()
});
}
// 取消选中
function deselectObject(mesh) {
mesh.dispatchEvent({
type: 'deselected'
});
}
// 使用示例
selectObject(mesh, new THREE.Vector3(1, 2, 3));完整的自定义事件系统
javascript
class InteractiveMesh extends THREE.Mesh {
constructor(geometry, material) {
super(geometry, material);
this.userData = {
isHovered: false,
isSelected: false
};
}
onHover(point) {
if (!this.userData.isHovered) {
this.userData.isHovered = true;
this.dispatchEvent({
type: 'hover',
point: point
});
}
}
onLeave() {
if (this.userData.isHovered) {
this.userData.isHovered = false;
this.dispatchEvent({
type: 'leave'
});
}
}
onSelect(point) {
if (!this.userData.isSelected) {
this.userData.isSelected = true;
this.dispatchEvent({
type: 'select',
point: point
});
}
}
onDeselect() {
if (this.userData.isSelected) {
this.userData.isSelected = false;
this.dispatchEvent({
type: 'deselect'
});
}
}
}
// 使用
const interactiveMesh = new InteractiveMesh(geometry, material);
interactiveMesh.addEventListener('hover', (event) => {
event.target.material.emissive.setHex(0x333333);
});
interactiveMesh.addEventListener('leave', (event) => {
event.target.material.emissive.setHex(0x000000);
});
interactiveMesh.addEventListener('select', (event) => {
event.target.material.color.setHex(0xff0000);
});
interactiveMesh.addEventListener('deselect', (event) => {
event.target.material.color.setHex(0x00ff00);
});事件工具类
事件管理器
javascript
class EventManager {
constructor() {
this.listeners = {};
}
// 添加事件监听
on(event, callback, context) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
const listener = { callback, context };
this.listeners[event].push(listener);
// 返回取消监听函数
return () => this.off(event, callback);
}
// 移除事件监听
off(event, callback) {
if (!this.listeners[event]) return;
if (callback) {
this.listeners[event] = this.listeners[event].filter(
listener => listener.callback !== callback
);
} else {
delete this.listeners[event];
}
}
// 触发事件
emit(event, ...args) {
if (!this.listeners[event]) return;
this.listeners[event].forEach(listener => {
listener.callback.apply(listener.context, args);
});
}
// 一次性监听
once(event, callback, context) {
const wrapper = (...args) => {
callback.apply(context, args);
this.off(event, wrapper);
};
return this.on(event, wrapper, context);
}
// 清除所有监听
clear() {
this.listeners = {};
}
}
// 使用示例
const eventManager = new EventManager();
// 监听事件
const unsubscribe = eventManager.on('objectSelected', (object) => {
console.log('选中对象:', object);
});
// 触发事件
eventManager.emit('objectSelected', mesh);
// 移除监听
unsubscribe();
// 或
eventManager.off('objectSelected');
// 一次性监听
eventManager.once('init', () => {
console.log('只执行一次');
});鼠标状态管理器
javascript
class MouseState {
constructor(element) {
this.element = element;
// 鼠标位置
this.position = new THREE.Vector2();
this.normalizedPosition = new THREE.Vector2();
this.delta = new THREE.Vector2();
// 按钮状态 [左, 中, 右]
this.buttons = [false, false, false];
// 修饰键状态
this.keys = {
shift: false,
ctrl: false,
alt: false,
meta: false
};
// 滚轮
this.wheelDelta = 0;
// 内部状态
this.previousPosition = new THREE.Vector2();
this.isFirstMove = true;
this.setupListeners();
}
setupListeners() {
this._onMouseDown = this.onMouseDown.bind(this);
this._onMouseUp = this.onMouseUp.bind(this);
this._onMouseMove = this.onMouseMove.bind(this);
this._onWheel = this.onWheel.bind(this);
this._onKeyDown = this.onKeyDown.bind(this);
this._onKeyUp = this.onKeyUp.bind(this);
this._onContextMenu = this.onContextMenu.bind(this);
this.element.addEventListener('mousedown', this._onMouseDown);
this.element.addEventListener('mouseup', this._onMouseUp);
this.element.addEventListener('mousemove', this._onMouseMove);
this.element.addEventListener('wheel', this._onWheel);
this.element.addEventListener('contextmenu', this._onContextMenu);
window.addEventListener('keydown', this._onKeyDown);
window.addEventListener('keyup', this._onKeyUp);
}
dispose() {
this.element.removeEventListener('mousedown', this._onMouseDown);
this.element.removeEventListener('mouseup', this._onMouseUp);
this.element.removeEventListener('mousemove', this._onMouseMove);
this.element.removeEventListener('wheel', this._onWheel);
this.element.removeEventListener('contextmenu', this._onContextMenu);
window.removeEventListener('keydown', this._onKeyDown);
window.removeEventListener('keyup', this._onKeyUp);
}
onMouseDown(event) {
this.buttons[event.button] = true;
this.updatePosition(event);
}
onMouseUp(event) {
this.buttons[event.button] = false;
}
onMouseMove(event) {
this.updatePosition(event);
}
onWheel(event) {
this.wheelDelta = event.deltaY;
}
onKeyDown(event) {
this.keys.shift = event.shiftKey;
this.keys.ctrl = event.ctrlKey;
this.keys.alt = event.altKey;
this.keys.meta = event.metaKey;
}
onKeyUp(event) {
this.keys.shift = event.shiftKey;
this.keys.ctrl = event.ctrlKey;
this.keys.alt = event.altKey;
this.keys.meta = event.metaKey;
}
onContextMenu(event) {
event.preventDefault();
}
updatePosition(event) {
const rect = this.element.getBoundingClientRect();
// 像素位置
this.position.set(
event.clientX - rect.left,
event.clientY - rect.top
);
// NDC 坐标
this.normalizedPosition.set(
(event.clientX - rect.left) / rect.width * 2 - 1,
-((event.clientY - rect.top) / rect.height) * 2 + 1
);
// 计算移动增量
if (!this.isFirstMove) {
this.delta.set(
this.position.x - this.previousPosition.x,
this.position.y - this.previousPosition.y
);
} else {
this.delta.set(0, 0);
this.isFirstMove = false;
}
this.previousPosition.copy(this.position);
}
// 便捷方法
isLeftButtonPressed() { return this.buttons[0]; }
isMiddleButtonPressed() { return this.buttons[1]; }
isRightButtonPressed() { return this.buttons[2]; }
isShiftPressed() { return this.keys.shift; }
isCtrlPressed() { return this.keys.ctrl; }
}
// 使用
const mouseState = new MouseState(renderer.domElement);
function animate() {
requestAnimationFrame(animate);
if (mouseState.isLeftButtonPressed()) {
// 按住左键时执行操作
camera.rotation.y += mouseState.delta.x * 0.01;
}
renderer.render(scene, camera);
}交互控制器
DragControls
用于拖拽场景中的对象。
javascript
import { DragControls } from 'three/addons/controls/DragControls.js';
const controls = new DragControls(objects, camera, renderer.domElement);
// 拖拽开始
controls.addEventListener('dragstart', (event) => {
console.log('开始拖拽:', event.object);
event.object.material.emissive.setHex(0x444444);
// 禁用轨道控制器
orbitControls.enabled = false;
});
// 拖拽中
controls.addEventListener('drag', (event) => {
console.log('拖拽位置:', event.object.position);
// 可以在这里添加约束逻辑
});
// 拖拽结束
controls.addEventListener('dragend', (event) => {
console.log('结束拖拽');
event.object.material.emissive.setHex(0x000000);
orbitControls.enabled = true;
});
// 鼠标悬停
controls.addEventListener('hoveron', (event) => {
console.log('悬停:', event.object);
document.body.style.cursor = 'grab';
});
controls.addEventListener('hoveroff', (event) => {
document.body.style.cursor = 'default';
});
// 销毁
controls.dispose();TransformControls
用于移动、旋转、缩放对象的可视化控制器。
javascript
import { TransformControls } from 'three/addons/controls/TransformControls.js';
const transformControls = new TransformControls(camera, renderer.domElement);
scene.add(transformControls);
// 选中对象
transformControls.attach(mesh);
// 变换模式
transformControls.setMode('translate'); // 移动
transformControls.setMode('rotate'); // 旋转
transformControls.setMode('scale'); // 缩放
// 设置控制器大小
transformControls.setSize(1);
// 监听变换事件
transformControls.addEventListener('dragging-changed', (event) => {
// 拖拽时禁用轨道控制器
orbitControls.enabled = !event.value;
});
transformControls.addEventListener('change', () => {
console.log('对象变换:', transformControls.object.position);
});
transformControls.addEventListener('objectChange', () => {
// 对象位置/旋转/缩放改变
});
// 键盘切换模式
window.addEventListener('keydown', (event) => {
switch (event.key.toLowerCase()) {
case 'w':
transformControls.setMode('translate');
break;
case 'e':
transformControls.setMode('rotate');
break;
case 'r':
transformControls.setMode('scale');
break;
case 'escape':
transformControls.detach();
break;
}
});
// 设置变换空间
transformControls.setSpace('world'); // 世界空间
transformControls.setSpace('local'); // 局部空间
// 启用/禁用特定轴
transformControls.showX = true;
transformControls.showY = true;
transformControls.showZ = false; // 禁止 Z 轴变换OrbitControls 与其他控制器的配合
javascript
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { TransformControls } from 'three/addons/controls/TransformControls.js';
const orbitControls = new OrbitControls(camera, renderer.domElement);
const transformControls = new TransformControls(camera, renderer.domElement);
scene.add(transformControls);
// 关键:当使用 TransformControls 时禁用 OrbitControls
transformControls.addEventListener('dragging-changed', (event) => {
orbitControls.enabled = !event.value;
});
// 点击选择对象
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
renderer.domElement.addEventListener('click', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(objects);
if (intersects.length > 0) {
transformControls.attach(intersects[0].object);
} else {
transformControls.detach();
}
});API 参考
EventDispatcher 类
所有 Three.js 对象的事件基类。
javascript
// 创建可派发事件的对象
const dispatcher = new THREE.EventDispatcher();
// 添加监听器
dispatcher.addEventListener('event', callback);
// 移除监听器
dispatcher.removeEventListener('event', callback);
// 检查是否有监听器
dispatcher.hasEventListener('event', callback);
// 派发事件
dispatcher.dispatchEvent({ type: 'event', data: {} });EventDispatcher 方法
| 方法 | 参数 | 说明 |
|---|---|---|
addEventListener(type, listener) | string, Function | 添加事件监听器 |
hasEventListener(type, listener) | string, Function | 检查是否有监听器 |
removeEventListener(type, listener) | string, Function | 移除事件监听器 |
dispatchEvent(event) | Object | 派发事件对象 |
DragControls 事件
| 事件名 | 说明 | event 属性 |
|---|---|---|
dragstart | 开始拖拽 | object |
drag | 拖拽中 | object |
dragend | 结束拖拽 | object |
hoveron | 鼠标悬停 | object |
hoveroff | 鼠标离开 | object |
TransformControls 事件
| 事件名 | 说明 | event 属性 |
|---|---|---|
dragging-changed | 拖拽状态变化 | value (boolean) |
change | 控制器变化 | - |
objectChange | 对象变换 | - |
mouseUp | 鼠标释放 | - |
TransformControls 属性
| 属性 | 类型 | 说明 |
|---|---|---|
mode | string | 当前模式:'translate', 'rotate', 'scale' |
object | Object3D | 当前附加的对象 |
showX | boolean | 显示 X 轴控制器 |
showY | boolean | 显示 Y 轴控制器 |
showZ | boolean | 显示 Z 轴控制器 |
size | number | 控制器大小 |
space | string | 变换空间:'world' 或 'local' |
常见问题
Q: 事件监听器内存泄漏?
A: 确保在不需要时移除监听器:
javascript
// 错误做法
window.addEventListener('resize', handleResize);
// 没有移除监听器
// 正确做法
class MyComponent {
constructor() {
this.handleResize = this.handleResize.bind(this);
window.addEventListener('resize', this.handleResize);
}
dispose() {
window.removeEventListener('resize', this.handleResize);
}
handleResize() {
// ...
}
}Q: 触摸事件和鼠标事件冲突?
A: 移动设备上触摸事件会触发模拟的鼠标事件:
javascript
// 防止触摸触发鼠标事件
renderer.domElement.addEventListener('touchstart', (event) => {
event.preventDefault();
}, { passive: false });
// 或检测设备类型
const isTouchDevice = 'ontouchstart' in window;
if (isTouchDevice) {
// 只绑定触摸事件
} else {
// 只绑定鼠标事件
}Q: 键盘事件不响应?
A: 键盘事件需要元素获得焦点:
javascript
// 确保画布可以获得焦点
renderer.domElement.tabIndex = 0;
renderer.domElement.focus();
// 或在 window 上监听
window.addEventListener('keydown', handler);Q: 事件处理影响帧率?
A: 使用节流或防抖:
javascript
// 节流
function throttle(fn, delay) {
let last = 0;
return function(...args) {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn.apply(this, args);
}
};
}
// 防抖
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 使用
renderer.domElement.addEventListener(
'mousemove',
throttle(handleMouseMove, 50)
);Q: TransformControls 和 OrbitControls 冲突?
A: 在使用 TransformControls 时禁用 OrbitControls:
javascript
transformControls.addEventListener('dragging-changed', (event) => {
orbitControls.enabled = !event.value;
});Q: 移动端双指缩放灵敏度问题?
A: 调整缩放系数:
javascript
let lastTouchDistance = 0;
const zoomSpeed = 0.01; // 调整这个值
renderer.domElement.addEventListener('touchmove', (event) => {
if (event.touches.length === 2) {
const distance = getTouchDistance(event.touches);
if (lastTouchDistance > 0) {
const delta = (distance - lastTouchDistance) * zoomSpeed;
camera.fov = Math.max(20, Math.min(120, camera.fov - delta * 10));
camera.updateProjectionMatrix();
}
lastTouchDistance = distance;
}
});最佳实践
- 及时清理监听器:组件销毁时移除事件监听器,防止内存泄漏
- 事件委托:对于多个相似元素,使用事件委托减少监听器数量
- 节流防抖:对频繁触发的事件使用节流或防抖优化性能
- 状态管理:使用状态对象管理按键、鼠标等状态
- 跨平台兼容:同时支持鼠标和触摸事件
- 可访问性:提供键盘操作支持
- 事件命名:自定义事件使用清晰、一致的命名规范