You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript中无法移除EventListener问题求助

TypeScript中移除EventListener失败的解决方案

问题场景

在TypeScript开发中,通过按钮控制布尔值movePick,当movePick为true时给document添加pointermove、mousedown、pointerup事件监听器实现物体拖动;但将movePick设为false时,无法移除这些监听器。仅在添加监听器的同一if代码块内执行移除操作才有效,尝试多种方案均无效。相关代码如下:

public moveObject(movePick: boolean, raycaster: THREE.Raycaster): void {
        const plane = new THREE.Plane();
        const pNormal = new THREE.Vector3(0, 1, 0); // plane's normal
        const planeIntersect = new THREE.Vector3(); // point of intersection with the plane
        const pIntersect = new THREE.Vector3(); // point of intersection with an object (plane's point)
        const shift = new THREE.Vector3(); // distance between position of an object and points of intersection with the object
        let isDragging = false;
        let dragObject: any;
        // events

        const pointermove = (event: PointerEvent) => {
            const rect = this.canvas.getBoundingClientRect();
            const x = event.clientX - rect.left
            const y = event.clientY - rect.top
            const pickPosition = { x: 0, y: 0 };
            pickPosition.x = (x / this.canvas.clientWidth) * 2 - 1;
            pickPosition.y = (y / this.canvas.clientHeight) * -2 + 1;
            raycaster.setFromCamera(pickPosition, this.camera);
            // console.log("movePICK IN POINTERMOVE",movePick)
            if (isDragging) {
                raycaster.ray.intersectPlane(plane, planeIntersect);
                dragObject.position.addVectors(planeIntersect, shift);
            }
        }

        const mousedown = () => {
            const intersects = raycaster.intersectObjects(this.scene.children);

            for (let i = 0; i < this.mesharr.length; i++) {
                if (intersects[0].object.name == this.mesharr[i].name && intersects[0].object.name != "Rail") {
                    pIntersect.copy(intersects[0].point);
                    plane.setFromNormalAndCoplanarPoint(pNormal, pIntersect);
                    shift.subVectors(intersects[0].object.position, intersects[0].point);
                    isDragging = true;
                    dragObject = intersects[0].object;
                }
            }
        }

        const pointerup = () => {
            isDragging = false;
            dragObject = null;
        }
        if (movePick) {
            document.addEventListener("pointermove", pointermove);
            document.addEventListener("mousedown", mousedown);
            document.addEventListener("pointerup", pointerup);
        } else {
            document.removeEventListener("pointermove", pointermove);
            document.removeEventListener("mousedown", mousedown);
            document.removeEventListener("pointerup", pointerup);
        }
    }

问题根源

每次调用moveObject函数时,都会重新创建pointermove、mousedown、pointerup这三个事件处理函数。添加监听器时用的是本次调用创建的函数引用,而移除时用的是下一次调用创建的新函数引用——两个引用指向不同的函数实例,removeEventListener无法匹配到之前添加的监听器,因此移除失败。

而在同一if代码块内移除有效,是因为此时添加和移除用的是同一个函数引用。

解决方案

将事件处理函数和拖动相关的变量(如plane、isDragging、dragObject等)提升为类的成员变量,确保每次调用moveObject时使用的是同一个函数引用。修改后的代码如下:

// 类成员变量
private plane = new THREE.Plane();
private pNormal = new THREE.Vector3(0, 1, 0);
private planeIntersect = new THREE.Vector3();
private pIntersect = new THREE.Vector3();
private shift = new THREE.Vector3();
private isDragging = false;
private dragObject: any;
private raycasterRef: THREE.Raycaster; // 保存raycaster引用

// 事件处理函数作为类成员方法
private pointermove = (event: PointerEvent) => {
    const rect = this.canvas.getBoundingClientRect();
    const x = event.clientX - rect.left
    const y = event.clientY - rect.top
    const pickPosition = { x: 0, y: 0 };
    pickPosition.x = (x / this.canvas.clientWidth) * 2 - 1;
    pickPosition.y = (y / this.canvas.clientHeight) * -2 + 1;
    this.raycasterRef.setFromCamera(pickPosition, this.camera);
    if (this.isDragging) {
        this.raycasterRef.ray.intersectPlane(this.plane, this.planeIntersect);
        this.dragObject.position.addVectors(this.planeIntersect, this.shift);
    }
}

private mousedown = () => {
    const intersects = this.raycasterRef.intersectObjects(this.scene.children);
    if (!intersects[0]) return; // 避免空指针
    for (let i = 0; i < this.mesharr.length; i++) {
        if (intersects[0].object.name == this.mesharr[i].name && intersects[0].object.name != "Rail") {
            this.pIntersect.copy(intersects[0].point);
            this.plane.setFromNormalAndCoplanarPoint(this.pNormal, this.pIntersect);
            this.shift.subVectors(intersects[0].object.position, intersects[0].point);
            this.isDragging = true;
            this.dragObject = intersects[0].object;
        }
    }
}

private pointerup = () => {
    this.isDragging = false;
    this.dragObject = null;
}

public moveObject(movePick: boolean, raycaster: THREE.Raycaster): void {
    this.raycasterRef = raycaster; // 更新raycaster引用
    if (movePick) {
        document.addEventListener("pointermove", this.pointermove);
        document.addEventListener("mousedown", this.mousedown);
        document.addEventListener("pointerup", this.pointerup);
    } else {
        document.removeEventListener("pointermove", this.pointermove);
        document.removeEventListener("mousedown", this.mousedown);
        document.removeEventListener("pointerup", this.pointerup);
    }
}

关键改动说明

  • 将所有拖动相关的状态变量和事件处理函数改为类的成员,确保每次调用moveObject时使用的是同一个函数实例。
  • 使用箭头函数定义事件处理方法,避免this指向问题,同时保证函数引用的一致性。
  • 添加raycasterRef成员变量保存传入的raycaster引用,供事件处理函数使用。
  • 在mousedown中增加空值判断,避免intersects[0]不存在时的报错。

内容的提问来源于stack exchange,提问作者Russix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 22:50:21