React useEffect中createElement创建元素出现重复翻倍问题
问题说明
- 排查范围仅需关注
useEffect相关逻辑,无需理解其余代码 - 异常现象:悬停
text元素时,自定义鼠标光标上的文本出现重复翻倍 - 实现目标:悬停对应文本元素时,将指定文本追加到自定义光标节点上
- 现有逻辑说明:所有事件绑定均在
useEffect中执行,选择createElement而非innerHTML实现节点追加,是为了满足后续向光标节点追加更多类型内容的扩展需求
原始问题代码
const cursorIntro = document.querySelector(".cursor"); const options = document.querySelector(".introduction .nav-options"); options.addEventListener("mousemove", function s(e) { var rect = options.getBoundingClientRect(); var x = e.clientX - rect.left; //x position within the element. var y = e.clientY - rect.top; cursorIntro.style.left = x + "px"; cursorIntro.style.top = y + "px"; }); function OnSelect() { const optionsSelection = document.querySelectorAll(".options"); optionsSelection.forEach((elem, i) => { elem.children[1].children[0].children[0].addEventListener( "mouseleave", () => { cursorIntro.removeChild(cursorIntro.lastChild); } ); elem.children[1].children[0].children[0].addEventListener( "mouseenter", () => { const createElement = document.createElement("h4"); createElement.innerText = elem.children[1].children[0].children[0].dataset.name; cursorIntro.appendChild(createElement); } ); }); } OnSelect();
问题根因
文本重复是两个逻辑缺陷共同导致的:
useEffect未配置正确的依赖项,且没有编写事件解绑的清理逻辑。组件重渲染时会重复给DOM元素绑定相同的事件监听,一次悬停会触发多次节点追加逻辑,直接导致文本数量翻倍。- 节点删除逻辑不可靠。使用
removeChild(cursorIntro.lastChild)删除文本时,没有校验被删除的节点是否为之前追加的文本节点,一旦光标内存在其他子元素,就会出现误删其他节点、旧文本残留的问题,后续悬停追加新节点时就会出现重复内容。
修复方案
- 给
useEffect传入空依赖数组[],保证绑定逻辑仅在组件挂载时执行一次;同时在useEffect的返回值中编写清理函数,组件卸载时解绑所有事件监听,避免重复绑定。 - 给动态追加的光标文本设置专属类名,追加前先清除已存在的同类型文本节点,避免重复添加;离开时仅删除对应类名的文本节点,不会误删光标内的其他内容。
修复后参考代码
useEffect(() => { const cursorIntro = document.querySelector(".cursor"); const options = document.querySelector(".introduction .nav-options"); // 抽离事件回调,方便后续解绑 const handleMouseMove = (e) => { const rect = options.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; cursorIntro.style.left = `${x}px`; cursorIntro.style.top = `${y}px`; }; options.addEventListener("mousemove", handleMouseMove); // 定义hover文本专属类名,用于精准DOM操作 const HOVER_TEXT_CLASS = "cursor-hover-text"; const handleMouseEnter = (e) => { // 追加前先清除已有hover文本,从根源避免重复 const existText = cursorIntro.querySelector(`.${HOVER_TEXT_CLASS}`); if (existText) existText.remove(); const textEl = document.createElement("h4"); textEl.className = HOVER_TEXT_CLASS; textEl.innerText = e.currentTarget.dataset.name; cursorIntro.appendChild(textEl); }; const handleMouseLeave = () => { // 仅删除hover文本节点,不影响光标其他内容 const existText = cursorIntro.querySelector(`.${HOVER_TEXT_CLASS}`); if (existText) existText.remove(); }; const optionsSelection = document.querySelectorAll(".options"); const bindTargetEls = []; optionsSelection.forEach((elem) => { const target = elem.children[1].children[0].children[0]; bindTargetEls.push(target); target.addEventListener("mouseenter", handleMouseEnter); target.addEventListener("mouseleave", handleMouseLeave); }); OnSelect(); // 清理函数:组件卸载时解绑所有事件,避免内存泄漏和重复绑定 return () => { options.removeEventListener("mousemove", handleMouseMove); bindTargetEls.forEach((el) => { el.removeEventListener("mouseenter", handleMouseEnter); el.removeEventListener("mouseleave", handleMouseLeave); }); }; }, []);
内容的提问来源于stack exchange,提问作者Myth Vince
相关产品推荐
相关产品推荐

