匿名函数无法移除事件监听?

基础知识

  • EventTarget .addEventListener() 方法将指定的监听器注册到 EventTarget 上,当该对象触发指定的事件时,指定的回调函数就会被执行。事件目标可以是一个文档上的元素 Element、Document 和 Window。
  • addEventListener() 的工作原理是将实现 EventListener 的函数或对象添加到调用它的 EventTarget 上的指定事件类型的事件侦听器列表中。如果要绑定的函数或对象已经被添加到列表中,该函数或对象不会被再次添加。
  • EventTarget 的 removeEventListener() 方法可以删除使用 EventTarget.addEventListener() 方法添加的事件。可以使用事件类型,事件侦听器函数本身,以及可能影响匹配过程的各种可选择的选项的组合来标识要删除的事件侦听器。
  • 调用 removeEventListener() 时,若传入的参数不能用于确定当前注册过的任何一个事件监听器,该函数不会起任何作用。 如果一个 EventTarget 上的事件监听器在另一监听器处理该事件时被移除,那么它将不能被事件触发。不过,它可以被重新绑定。

问题描述

  • 代码如下,可以手动添加/移除监听事件;在操作过程中,添加监听事件成功,移除监听事件失败。监听的函数仍然生效,会不断打印出"The user is scrolling!"和index的值。
  • 移除监听器失效的原因在于,useEffect 中为 scroll 事件添加的监听器是一个匿名函数:() => handleScroll(index)。 由于匿名函数的引用不同,removeEventListener 无法匹配到正确的函数引用,因此无法有效移除监听器。
import React, { useState, useEffect } from "react";

const App = () => {
  const [isListening, setIsListening] = useState(false);

  const handleScroll = useCallback((index) => {
    console.log("The user is scrolling!", index);
  }, []);

  // 使用useEffect添加和移除事件监听器
  useEffect(() => {
    if (isListening) {
      let index = 1;
      console.log("设置事件监听!");
      window.addEventListener("scroll", () => handleScroll(index));
      return () => {
        window.removeEventListener("scroll", handleScroll);
      };
    }
  }, [isListening, handleScroll]);

  // 手动控制监听器的添加和移除
  const toggleListener = () => {
    setIsListening((prevIsListening) => !prevIsListening);
  };

  // 手动移除监听器
  const removeListener = () => {
    if (isListening) {
      console.log("移除事件监听!");
      window.removeEventListener("scroll", handleScroll);
      setIsListening(false); // 更新状态以反映监听器已被移除
    }
  };

  return (
    <div style={{ height: "1000vh" }}>
      <p>滚动页面以查看控制台日志。</p>
      <button onClick={toggleListener}>
        {isListening ? "停止监听滚动" : "开始监听滚动"}
      </button>
      <button onClick={removeListener}>手动移除滚动监听</button>
    </div>
  );
};

export default App;

追根溯源

  • MDN文档示例:
const els = document.getElementsByTagName("*");

// 例一
for (let i = 0; i < els.length; i++) {
  els[i].addEventListener(
    "click",
    (e) => {
      /* 处理点击事件 */
    },
    false,
  );
}

// 例二
function processEvent(e) {
  /* 处理同样的点击事件 */
}

for (let i = 0; i < els.length; i++) {
  els[i].addEventListener("click", processEvent, false);
}

  • 在上面的第一个例子中,一个新的(匿名)函数在每次循环中被创建一次。在第二个例子中,与之前的匿名函数功能相同的函数被用作事件监听器,但后者所带来的内存开销要更小一点,因为函数只被声明过一次。
  • 此外,在第一个例子中,我们不能调用 removeEventListener(),因为我们没有保留任何对匿名函数的引用(在例子的情况中,是没有保存对循环中创建的多个匿名函数的引用)。而在第二个例子中,processEvent 是一个可被引用的函数,因此可以调用 myElement.removeEventListener(“click”, processEvent, false)。
  • 实际上,真正影响内存的并不是没有保持函数引用,而是没有 保持静态的函数引用 – 在整个程序生命周期内保持不变的函数引用。

问题解决

若希望在 useEffect 中通过 () => handleScroll(index) 设置的监听器能够正确移除,需要确保 removeEventListener 能准确匹配添加的监听器。可以通过为匿名函数显式引用来解决。

import React, { useState, useEffect, useCallback } from "react";

const App = () => {
  const [isListening, setIsListening] = useState(false);

  const handleScroll = useCallback((index) => {
    console.log("The user is scrolling!", index);
  }, []);

  // 用于保存事件监听器函数的引用
  const scrollHandlerRef = React.useRef(null);

  useEffect(() => {
    if (isListening) {
      let index = 1;
      console.log("设置事件监听!");

      // 将匿名函数赋值给 ref
      scrollHandlerRef.current = () => handleScroll(index);
      window.addEventListener("scroll", scrollHandlerRef.current);

      return () => {
        console.log("移除事件监听!");
        if (scrollHandlerRef.current) {
          window.removeEventListener("scroll", scrollHandlerRef.current);
        }
      };
    }
  }, [isListening, handleScroll]);

  // 手动控制监听器的添加和移除
  const toggleListener = () => {
    setIsListening((prevIsListening) => !prevIsListening);
  };

  const removeListener = () => {
    if (isListening && scrollHandlerRef.current) {
      console.log("手动移除事件监听!");
      window.removeEventListener("scroll", scrollHandlerRef.current);
      setIsListening(false); // 更新状态以反映监听器已被移除
    }
  };

  return (
    <div style={{ height: "1000vh" }}>
      <p>滚动页面以查看控制台日志。</p>
      <button onClick={toggleListener}>
        {isListening ? "停止监听滚动" : "开始监听滚动"}
      </button>
      <button onClick={removeListener}>手动移除滚动监听</button>
    </div>
  );
};

export default App;
  • 使用 scrollHandlerRef(一个 ref)来存储动态生成的事件监听函数,确保在移除事件监听时可以引用到正确的函数。
  • scrollHandlerRef.current 是一个闭包,可以捕获当前的 index 值,并在 addEventListener 和 removeEventListener 中使用。
  • 在 useEffect 的清理函数中,通过 scrollHandlerRef 来移除正确的事件监听器。

多个场景

  • 同样,可以通过循环为多个事件监听器动态地添加和移除,关键是要为每个事件监听器函数保留一个稳定的引用,同时确保每个监听器的管理逻辑清晰。以下是实现方案:
import React, { useState, useEffect, useCallback, useRef } from "react";

const App = () => {
  const [isListening, setIsListening] = useState(false);

  const handleScroll = useCallback((index) => {
    console.log(`滚动事件触发,Index: ${index}`);
  }, []);

  const eventHandlers = useRef([]); // 保存所有事件监听器的引用

  useEffect(() => {
    if (isListening) {
      console.log("设置多个事件监听器!");

      // 动态创建监听器
      [1, 2].forEach((index) => {
        const listener = () => handleScroll(index);
        eventHandlers.current[index] = listener; // 保存每个监听器引用
        window.addEventListener("scroll", listener);
      });

      return () => {
        console.log("移除所有事件监听器!");
        [1, 2].forEach((index) => {
          const listener = eventHandlers.current[index];
          if (listener) {
            window.removeEventListener("scroll", listener);
          }
        });
        eventHandlers.current = []; // 清空监听器引用
      };
    }
  }, [isListening, handleScroll]);

  // 控制监听器的添加和移除
  const toggleListener = () => {
    setIsListening((prevIsListening) => !prevIsListening);
  };

  const removeAllListeners = () => {
    console.log("手动移除所有事件监听器!");
    [1, 2].forEach((index) => {
      const listener = eventHandlers.current[index];
      if (listener) {
        window.removeEventListener("scroll", listener);
      }
    });
    eventHandlers.current = []; // 清空监听器引用
    setIsListening(false); // 更新状态
  };

  return (
    <div style={{ height: "1000vh" }}>
      <p>滚动页面以查看控制台日志。</p>
      <button onClick={toggleListener}>
        {isListening ? "停止监听滚动" : "开始监听滚动"}
      </button>
      <button onClick={removeAllListeners}>手动移除所有滚动监听</button>
    </div>
  );
};

export default App;

  • eventHandlers 用于保存每个监听器的引用:通过一个数组 eventHandlers.current 保存每个监听器的引用,索引与动态参数(如 index)关联。这样可以在移除监听器时通过索引访问对应的函数。
  • 动态添加多个监听器:使用 forEach 遍历要添加监听器的参数(如 [1, 2]),为每个参数生成对应的事件监听器函数,并将它添加到 window 和 eventHandlers 中。
  • 移除所有监听器:在 useEffect 的清理函数和手动移除函数中,通过遍历 eventHandlers.current 来移除所有监听器。
  • 清空监听器引用:移除监听器后,清空 eventHandlers.current 以防止遗留无效引用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/965731.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

IDEA2024版本创建Sping项目无法选择Java 8

目录 一、背景二、解决方式&#xff08;替换创建项目的源地址&#xff09; 一、背景 IDEA2024创建一个springboot的项目&#xff0c;本地安装的是1.8&#xff0c;但是在使用Spring Initializr创建项目时&#xff0c;发现版本只有17、21、23。 二、解决方式&#xff08;替换创…

C++11(四)

目录 包装器 function包装器 bind绑定 更改实参传递的顺序和实参传递的个数 线程库 本期我们将继续进行C11新特性的学习。 包装器 function包装器 function包装器&#xff0c;我们也称之为适配器&#xff0c;本质上就是一个类模板&#xff0c;为什么要引入function包…

MySQL 数据库编程-C++

目录 1 数据库基本知识 1.1 MYSQL常见命令 1.2 SQL注入 1.3 ORM框架 1 数据库基本知识 MySQL 为关系型数据库(Relational Database Management System), 这种所谓的"关系型"可以理解为"表格"的概念, 一个关系型数据库由一个或数个表格组成&#xff1a…

【算法篇】贪心算法

目录 贪心算法 贪心算法实际应用 一&#xff0c;零钱找回问题 二&#xff0c;活动选择问题 三&#xff0c;分数背包问题 将数组和减半的最小操作次数 最大数 贪心算法 贪心算法&#xff0c;是一种在每一步选择中都采取当前状态下的最优策略&#xff0c;期望得到全局最优…

5 计算机网络

5 计算机网络 5.1 OSI/RM七层模型 5.2 TCP/IP协议簇 5.2.1:常见协议基础 一、 TCP是可靠的&#xff0c;效率低的&#xff1b; 1.HTTP协议端口默认80&#xff0c;HTTPSSL之后成为HTTPS协议默认端口443。 2.对于0~1023一般是默认的公共端口不需要注册&#xff0c;1024以后的则需…

动态规划LeetCode-1035.不相交的线

在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。 现在&#xff0c;可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线&#xff0c;这些直线需要同时满足&#xff1a; nums1[i] nums2[j]且绘制的直线不与任何其他连线&#xff08;非水平线&#xff09;相…

禅道社区版项目管理软件部署(记录篇)

系统要求&#xff08;这里推荐使用docker容器化方式&#xff09;安装前的准备Docker快速安装最后通过查看地址验证是否部署成功开始界面化安装配置 禅道&#xff08;ZenTao&#xff09;是一款国产开源的项目管理软件&#xff0c;专注于敏捷开发流程&#xff0c;支持 Scrum 和 K…

数据结构-基础

1、概念&#xff1a; 程序 数据结构 算法 2、程序的好坏 可读性&#xff0c;稳定性&#xff0c;扩展性&#xff0c;时间复杂度&#xff0c;空间复杂度。 3、数据结构 是指存储、组织数据的方式&#xff0c;以便高效地进行访问和修改。通过选择适当的数据结构&#xff0c; 能…

从零开始:OpenCV 图像处理快速入门教程

文章大纲 第1章 OpenCV 概述 1.1 OpenCV的模块与功能  1.2 OpenCV的发展 1.3 OpenCV的应用 第2章 基本数据类型 2.1 cv::Vec类 2.2 cv&#xff1a;&#xff1a;Point类 2.3 cv&#xff1a;&#xff1a;Rng类 2.4 cv&#xff1a;&#xff1a;Size类 2.5 cv&#xff1a;&…

1-kafka服务端之延时操作前传--时间轮

文章目录 背景时间轮层级时间轮时间轮降级kafka中的时间轮kafka如何进行时间轮运行 背景 Kafka中存在大量的延时操作&#xff0c;比如延时生产、延时拉取和延时删除等。Kafka并没有使用JDK自带的Timer或DelayQueue来实现延时的功能&#xff0c;而是基于时间轮的概念自定义实现…

Java 注解使用教程

简介 Java 1.5 引入了注解&#xff0c;现在它在 Java EE 框架&#xff08;如 Hibernate、Jersey 和 Spring &#xff09;中被大量使用。Java 注释是该语言的一个强大特性&#xff0c;用于向 Java 代码中添加元数据。它们不直接影响程序逻辑&#xff0c;但可以由工具、库或框架…

第17章 读写锁分离设计模式(Java高并发编程详解:多线程与系统设计)

1.场景描述 对资源的访问一般包括两种类型的动作——读和写(更新、删除、增加等资源会发生变化的动作)&#xff0c;如果多个线程在某个时刻都在进行资源的读操作&#xff0c;虽然有资源的竞争&#xff0c;但是这种竞争不足以引起数据不一致的情况发生&#xff0c;那么这个时候…

强化学习 DAY1:什么是 RL、马尔科夫决策、贝尔曼方程

第一部分 RL基础&#xff1a;什么是RL与MRP、MDP 1.1 入门强化学习所需掌握的基本概念 1.1.1 什么是强化学习&#xff1a;依据策略执行动作-感知状态-得到奖励 强化学习里面的概念、公式&#xff0c;相比ML/DL特别多&#xff0c;初学者刚学RL时&#xff0c;很容易被接连不断…

【STM32系列】利用MATLAB配合ARM-DSP库设计FIR数字滤波器(保姆级教程)

ps.源码放在最后面 设计IIR数字滤波器可以看这里&#xff1a;利用MATLAB配合ARM-DSP库设计IIR数字滤波器&#xff08;保姆级教程&#xff09; 前言 本篇文章将介绍如何利用MATLAB与STM32的ARM-DSP库相结合&#xff0c;简明易懂地实现FIR低通滤波器的设计与应用。文章重点不在…

DeepSeek-R1 本地电脑部署 Windows系统 【轻松简易】

本文分享在自己的本地电脑部署 DeepSeek&#xff0c;而且轻松简易&#xff0c;快速上手。 这里借助Ollama工具&#xff0c;在Windows系统中进行大模型部署~ 1、安装Ollama 来到官网地址&#xff1a;Download Ollama on macOS 点击“Download for Windows”下载安装包&#x…

Llama最新开源大模型Llama3.1

Meta公司于2024年7月23日发布了最新的开源大模型Llama 3.1&#xff0c;这是其在大语言模型领域的重要进展。以下是关于Llama 3.1的详细介绍&#xff1a; 参数规模与训练数据 Llama 3.1拥有4050亿&#xff08;405B&#xff09;参数&#xff0c;是目前开源领域中参数规模最大的…

Linux之安装docker

一、检查版本和内核是否合格 Docker支持64位版本的CentOS 7和CentOS 8及更高版本&#xff0c;它要求Linux内核版本不低于3.10。 检查版本 cat /etc/redhat-release检查内核 uname -r二、Docker的安装 1、自动安装 Docker官方和国内daocloud都提供了一键安装的脚本&#x…

2022年全国职业院校技能大赛网络系统管理赛项模块A:网络构建(样题3)-网络部分解析-附详细代码

目录 附录1:拓扑图 附录2:地址规划表 1.SW1 2.SW2 3.SW3 4.SW4 5.SW5 6.SW6 7.SW7 8.R1 9.R2 10.R3 11.AC1 12.AC2 13.AP2 14.AP3 15.EG1 16.EG2 附录1:拓扑图 附录2:地址规划表 设备

Vim跳转文件及文件行结束符EOL

跳转文件 gf 从当前窗口打开那个文件的内容&#xff0c;操作方式&#xff1a;让光标停在文件名上&#xff0c;输入gf。 Ctrlo 从打开的文件返回之前的窗口 Ctrlwf 可以在分割的窗口打开跳转的文件&#xff0c;不过在我的实验不是次次都成功。 统一行尾格式 文本文件里存放的…

《Angular之image loading 404》

前言&#xff1a; 千锤万凿出深山&#xff0c;烈火焚烧若等闲。 正文&#xff1a; 一。问题描述 页面加载图片&#xff0c;报错404 二。问题定位 页面需要加载图片&#xff0c;本地开发写成硬编码的形式请求图片资源&#xff1a; 然而部署到服务器上报错404 三。解决方案 正确…