HTML添加指定类时停止JS运行,移除类时恢复运行的实现方案
问题描述
我做了一个主题切换功能,能改变UI外观,同时有个自定义JS光标,但Retro主题下不需要它。因为光标涉及大量指针位置计算,我想在html元素添加.retro类时完全停止脚本运行,不是只隐藏元素却让脚本继续跑。
现有三种主题模式:
- 默认模式(页面加载默认启用)
- Dark模式
- Retro模式(需停止光标脚本)
我试了当前的实现:初始给html加.retro类时脚本确实停了,点击切换按钮后脚本能恢复,但只能生效一次。我想知道问题出在哪,以及最优解法。核心需求是:只要html没.retro类就运行光标脚本;移除.retro类时重新启动脚本。
现有代码
Toggle 逻辑
const html = document.querySelector("html"); const button = document.querySelector(".contrast__link"); button.addEventListener("click", (e) => { e.preventDefault(); if (html.classList.contains("dark-mode")) { html.classList.remove("dark-mode"); html.classList.add("retro"); let slideIndex = swiper.activeIndex; swiper.destroy(true, true); swiper = initSwiper("slide", false, 999999999); // Retro: 滑动切换,无自动播放/循环 swiper.slideTo(slideIndex, 0); } else if (html.classList.contains("retro")) { html.classList.remove("retro"); let slideIndex = swiper.activeIndex; swiper.destroy(true, true); swiper = initSwiper("fade", true, 1200); // 默认:淡入切换,自动播放+循环 swiper.slideTo(slideIndex, 0); } else { html.classList.add("dark-mode"); } });
Cursor 逻辑
function createHandler(callback) { return function(event) { if (!document.documentElement.classList.contains('retro')) { callback.call(this, event) } } } window.addEventListener('mousemove', createHandler(function() { var cursor = document.querySelector(".cursor"); var cursorTrail = document.querySelector(".cursor-trail"); var a = document.querySelectorAll("a"); var timeout; window.addEventListener( "mousemove", function(e) { var x = e.clientX; var y = e.clientY; cursor.style.transform = `translate(${x - 2}px, ${y - 2}px)`; if (!timeout) { timeout = setTimeout(function() { timeout = null; cursorTrail.style.transform = `translate(${x - 16}px, ${y - 16}px)`; }, 24); } }, false ); document.addEventListener("mousedown", function() { cursor.classList.add("cursor--click"); }); document.addEventListener("mouseup", function() { cursor.classList.remove("cursor--click"); }); let links = document.querySelectorAll('a, #hello'); links.forEach((item) => { item.addEventListener("mouseover", () => { cursorTrail.classList.add("cursor-trail--hover"); }); item.addEventListener("mouseleave", () => { cursorTrail.classList.remove("cursor-trail--hover"); }); }); a.forEach((item) => { const interaction = item.dataset.interaction; item.addEventListener("mouseover", () => { cursor.classList.add(interaction); }); item.addEventListener("mouseleave", () => { cursor.classList.remove(interaction); }); }); }))
CSS
.contrast__link { background: white; } .cursor { background: red; height: 20px; width: 20px; position: fixed; } .dark-mode { background: black; } .retro { background: blue; }
HTML
<a href="" class="contrast__link">Toggle Classes</a> <div class="cursor"><span></span></div> <div class="cursor-trail"></div>
现有实现的问题
- 重复绑定事件:你把整个光标逻辑嵌套在一个
mousemove事件回调里,每次触发外层mousemove(非Retro模式时),就会重复给window、document、所有链接绑定新的事件监听器。第一次切换出Retro模式时绑定一次,第二次切换回来再切出去又绑定一次,导致事件叠加,逻辑混乱,最终失效。 - 条件判断位置错误:外层的
createHandler只是控制光标逻辑是否执行一次,但内部的事件绑定是一次性的,无法动态响应后续的主题切换。
最优解决方案
核心思路:把光标逻辑封装成可启动/停止的独立模块,在主题切换时直接调用模块的启动/停止方法,精准控制脚本状态,避免重复绑定事件。
步骤1:封装光标模块
/* Cursor 模块 */ const CursorModule = (function() { let cursor, cursorTrail, links, aTags; let timeout; let isActive = false; // 基础事件处理函数 function handleMouseMove(e) { const x = e.clientX; const y = e.clientY; cursor.style.transform = `translate(${x - 2}px, ${y - 2}px)`; if (!timeout) { timeout = setTimeout(() => { timeout = null; cursorTrail.style.transform = `translate(${x - 16}px, ${y - 16}px)`; }, 24); } } function handleMouseDown() { cursor.classList.add("cursor--click"); } function handleMouseUp() { cursor.classList.remove("cursor--click"); } function handleLinkHover() { cursorTrail.classList.add("cursor-trail--hover"); } function handleLinkLeave() { cursorTrail.classList.remove("cursor-trail--hover"); } function handleInteractionHover(e) { const interaction = e.target.dataset.interaction; if (interaction) cursor.classList.add(interaction); } function handleInteractionLeave(e) { const interaction = e.target.dataset.interaction; if (interaction) cursor.classList.remove(interaction); } // 初始化DOM元素引用 function initElements() { cursor = document.querySelector(".cursor"); cursorTrail = document.querySelector(".cursor-trail"); links = document.querySelectorAll('a, #hello'); aTags = document.querySelectorAll("a"); } // 绑定所有事件 function bindEvents() { window.addEventListener("mousemove", handleMouseMove); document.addEventListener("mousedown", handleMouseDown); document.addEventListener("mouseup", handleMouseUp); links.forEach(item => { item.addEventListener("mouseover", handleLinkHover); item.addEventListener("mouseleave", handleLinkLeave); }); aTags.forEach(item => { item.addEventListener("mouseover", handleInteractionHover); item.addEventListener("mouseleave", handleInteractionLeave); }); } // 解绑所有事件 function unbindEvents() { window.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mousedown", handleMouseDown); document.removeEventListener("mouseup", handleMouseUp); links.forEach(item => { item.removeEventListener("mouseover", handleLinkHover); item.removeEventListener("mouseleave", handleLinkLeave); }); aTags.forEach(item => { item.removeEventListener("mouseover", handleInteractionHover); item.removeEventListener("mouseleave", handleInteractionLeave); }); // 清除未执行的定时器 if (timeout) clearTimeout(timeout); } // 启动光标脚本 function start() { if (isActive) return; initElements(); bindEvents(); isActive = true; // 显示光标元素 cursor.style.display = "block"; cursorTrail.style.display = "block"; } // 停止光标脚本 function stop() { if (!isActive) return; unbindEvents(); isActive = false; // 隐藏光标元素 cursor.style.display = "none"; cursorTrail.style.display = "none"; } return { start, stop }; })();
步骤2:修改主题切换逻辑,联动光标模块
/* Toggle 逻辑 */ const html = document.querySelector("html"); const button = document.querySelector(".contrast__link"); // 页面加载时初始化光标状态 function initCursorState() { if (!html.classList.contains('retro')) { CursorModule.start(); } else { CursorModule.stop(); } } // 主题切换点击事件 button.addEventListener("click", (e) => { e.preventDefault(); if (html.classList.contains("dark-mode")) { html.classList.remove("dark-mode"); html.classList.add("retro"); // 切换到Retro模式,停止光标 CursorModule.stop(); let slideIndex = swiper.activeIndex; swiper.destroy(true, true); swiper = initSwiper("slide", false, 999999999); swiper.slideTo(slideIndex, 0); } else if (html.classList.contains("retro")) { html.classList.remove("retro"); // 退出Retro模式,启动光标 CursorModule.start(); let slideIndex = swiper.activeIndex; swiper.destroy(true, true); swiper = initSwiper("fade", true, 1200); swiper.slideTo(slideIndex, 0); } else { html.classList.add("dark-mode"); // Dark模式下保持光标运行 CursorModule.start(); } }); // 页面加载完成后初始化光标 document.addEventListener('DOMContentLoaded', initCursorState);
步骤3:保留原有CSS和HTML
CSS、HTML部分无需修改,沿用你原来的代码即可。
方案优势
- 无重复事件绑定:通过显式的绑定/解绑方法,确保事件只绑定一次,切换主题时正确清理,避免逻辑混乱。
- 精准控制脚本状态:每次主题切换时直接调用光标模块的启动/停止方法,完全停止或恢复脚本运行,节省性能。
- 模块化设计:光标逻辑封装成独立模块,代码结构清晰,便于后续维护和扩展。
内容的提问来源于stack exchange,提问作者user1406440
相关产品推荐
相关产品推荐

