Почему this неопределён в Three.js setAnimationLoop
Узнайте, почему this становится неопределённым в WebGLRenderer.setAnimationLoop при использовании методов класса в Three.js, и найдите решения для сохранения контекста анимации.
Почему this равен undefined внутри WebGLRenderer().setAnimationLoop(), когда я использую метод класса в Three.js?
Я новичок в Three.js и инициализирую сцену, рендерер и другие компоненты внутри класса. У класса есть метод animate, и в конструкторе я вызываю this.renderer.setAnimationLoop(this.animate). Однако, когда я делаю console.log(this) внутри метода animate, он выводит undefined. Я сначала вызвал animate() напрямую в конструкторе, и тогда this логировался корректно, но после вызова setAnimationLoop он стал undefined. Я считаю, что Three.js рассматривает this.animate как обычную функцию, а не как метод, поэтому this недоступен. Как правильно сохранить контекст this в моём анимационном цикле?
Текущий контекст this становится undefined внутри метода класса при использовании WebGLRenderer().setAnimationLoop() из‑за того, что JavaScript теряет правильный контекст выполнения, когда функции передаются как обратные вызовы. Когда вы передаёте this.animate в setAnimationLoop, вы передаёте только ссылку на функцию, которая теряет привязку к экземпляру класса, и при выполнении цикла анимации this оказывается undefined.
Содержание
- Понимание проблемы привязки контекста
- Почему setAnimationLoop теряет контекст
- Решение 1: Стрелочные функции
- Решение 2: Явная привязка с .bind()
- Решение 3: Создание обёртки
- Решение 4: Поля класса со стрелочными функциями
- Лучшие практики и рекомендации
- Полный рабочий пример
- Вывод
Понимание проблемы привязки контекста
Проблема, с которой вы столкнулись, является фундаментальным поведением JavaScript, связанным с тем, как работает привязка this. Когда вы вызываете this.renderer.setAnimationLoop(this.animate), вы передаёте ссылку на ваш метод animate, но без правильной привязки контекста.
Как описано в документации Discover three.js, цикл анимации должен сохранять правильный контекст, чтобы иметь доступ к свойствам и методам класса. Проблема возникает, потому что setAnimationLoop внутри себя выполняет вашу функцию в другом контексте, отличном от экземпляра вашего класса.
Почему setAnimationLoop теряет контекст
Когда setAnimationLoop получает ссылку на вашу функцию, она сохраняет её и вызывает на последующих кадрах анимации. В этот момент функция выполняется как отдельная функция, а не как метод, привязанный к экземпляру вашего класса.
Это типичный паттерн в JavaScript‑обратных вызовах, как отмечено в обсуждении на форуме three.js. В теме форума много разработчиков сталкиваются с той же проблемой при рефакторинге кода в классы.
Фундаментальная причина: в JavaScript this определяется тем, как вызывается функция, а не где она объявлена. Когда setAnimationLoop вызывает вашу функцию, она не сохраняет исходный контекст this.
Решение 1: Стрелочные функции
Стрелочные функции автоматически сохраняют контекст this из внешней лексической области. Это часто самый чистый способ:
class RenderEngine {
constructor() {
// Инициализация сцены, рендерера, камеры и т.д.
this.renderer = new THREE.WebGLRenderer();
// ...другие инициализации
// Используем стрелочную функцию, чтобы сохранить контекст
this.renderer.setAnimationLoop(() => {
this.animate();
});
}
animate() {
// Теперь 'this' будет ссылаться на экземпляр RenderEngine
console.log(this); // Должен вывести экземпляр RenderEngine
// Ваш код анимации здесь
this.updateScene();
this.renderer.render(this.scene, this.camera);
}
updateScene() {
// Обновление объектов сцены
}
}
Стрелочная функция создаёт замыкание, которое сохраняет this из области конструктора, гарантируя, что при вызове animate() контекст будет правильным.
Решение 2: Явная привязка с .bind()
Можно явно привязать ваш метод к экземпляру класса:
class RenderEngine {
constructor() {
// ...инициализация
// Привязываем метод animate к экземпляру класса
this.renderer.setAnimationLoop(this.animate.bind(this));
}
animate() {
console.log(this); // Будет экземпляр RenderEngine
// ...код анимации
}
}
Метод .bind(this) создаёт новую функцию, у которой ключевое слово this установлено в переданное значение, обеспечивая правильную привязку контекста.
Решение 3: Создание обёртки
Можно создать обёрточную функцию, которая сохраняет контекст:
class RenderEngine {
constructor() {
// ...инициализация
// Создаём обёртку, сохраняющую контекст
const animationWrapper = () => {
this.animate();
};
this.renderer.setAnimationLoop(animationWrapper);
}
animate() {
console.log(this); // Будет экземпляр RenderEngine
// ...код анимации
}
}
Этот подход похож на использование стрелочных функций, но даёт больше гибкости, если нужно передать дополнительные параметры или создать более сложную логику обёртки.
Решение 4: Поля класса со стрелочными функциями
Современный JavaScript позволяет определять поля класса со стрелочными функциями, которые автоматически привязываются к экземпляру:
class RenderEngine {
constructor() {
// ...инициализация
// Это будет правильно привязано к экземпляру
this.animationLoop = () => {
this.animate();
};
this.renderer.setAnimationLoop(this.animationLoop);
}
animate() {
console.log(this); // Будет экземпляр RenderEngine
// ...код анимации
}
}
Этот подход элегантен, потому что привязка происходит при создании экземпляра класса, а не при вызове метода.
Лучшие практики и рекомендации
Предпочтительный подход: Стрелочные функции
Самый распространённый и читаемый способ — использовать стрелочные функции, как показано в книге Discover three.js. Это чистый и поддерживаемый код.
Избегайте прямых ссылок на методы
Никогда не передавайте this.animate напрямую в setAnimationLoop без правильной привязки:
// Это потеряет контекст
this.renderer.setAnimationLoop(this.animate);
Учитывайте влияние на производительность
Обратите внимание на влияние на производительность. Стрелочные функции создают новые функции, поэтому в критичных по производительности приложениях можно предпочесть .bind() или поля класса.
Остановка цикла анимации
Не забывайте корректно останавливать цикл анимации, когда это необходимо:
class RenderEngine {
constructor() {
// ...инициализация
this.renderer.setAnimationLoop(() => {
this.animate();
});
}
stop() {
// Корректно останавливаем цикл анимации
this.renderer.setAnimationLoop(null);
}
}
Как упомянуто в обсуждении GitHub, установка setAnimationLoop(null) является правильным способом остановки цикла.
Полный рабочий пример
Ниже приведён полный рабочий пример, демонстрирующий правильный подход:
import * as THREE from 'three';
class SceneRenderer {
constructor() {
// Инициализация сцены
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
// Настройка рендерера
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(this.renderer.domElement);
// Создаём простую кубическую геометрию
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
this.cube = new THREE.Mesh(geometry, material);
this.scene.add(this.cube);
this.camera.position.z = 5;
// Запускаем цикл анимации с правильным контекстом
this.startAnimation();
}
startAnimation() {
// Используем стрелочную функцию для сохранения 'this'
this.renderer.setAnimationLoop(() => {
this.animate();
});
}
animate() {
// 'this' правильно привязан к экземпляру SceneRenderer
console.log('Animating with this:', this);
// Поворот куба
this.cube.rotation.x += 0.01;
this.cube.rotation.y += 0.01;
// Рендер сцены
this.renderer.render(this.scene, this.camera);
}
stopAnimation() {
this.renderer.setAnimationLoop(null);
}
// Обработка изменения размера окна
handleResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
}
// Использование
const renderer = new SceneRenderer();
// Добавляем обработчик события изменения размера
window.addEventListener('resize', () => {
renderer.handleResize();
});
Этот пример демонстрирует чистую реализацию, сохраняющую правильный контекст во всём цикле анимации.
Вывод
Проблема this в WebGLRenderer().setAnimationLoop() является типичным поведением JavaScript, которое легко решить с помощью правильных техник привязки контекста. Ключевые выводы:
- Проблема: JavaScript теряет контекст
this, когда методы передаются как обратные вызовы без привязки. - Решения: Используйте стрелочные функции,
.bind(), обёртки или поля класса, чтобы сохранить контекст. - Лучший практический совет: Стрелочные функции обычно являются самым чистым и читаемым решением.
- Остановка цикла: Всегда используйте
renderer.setAnimationLoop(null)для корректной остановки. - Производительность: При выборе метода привязки учитывайте конкретные требования к производительности.
Применив любое из этих решений правильно, вы сохраните корректный контекст this в цикле анимации Three.js, что позволит обращаться к свойствам и методам класса как ожидается.