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

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>
现有实现的问题
  1. 重复绑定事件:你把整个光标逻辑嵌套在一个mousemove事件回调里,每次触发外层mousemove(非Retro模式时),就会重复给window、document、所有链接绑定新的事件监听器。第一次切换出Retro模式时绑定一次,第二次切换回来再切出去又绑定一次,导致事件叠加,逻辑混乱,最终失效。
  2. 条件判断位置错误:外层的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部分无需修改,沿用你原来的代码即可。

方案优势

  1. 无重复事件绑定:通过显式的绑定/解绑方法,确保事件只绑定一次,切换主题时正确清理,避免逻辑混乱。
  2. 精准控制脚本状态:每次主题切换时直接调用光标模块的启动/停止方法,完全停止或恢复脚本运行,节省性能。
  3. 模块化设计:光标逻辑封装成独立模块,代码结构清晰,便于后续维护和扩展。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 04:20:31