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

JS逐字变色hover动画悬停同单词不同字母即重置的原因与修复

问题触发根源
  • 问题本质来自事件类型的选择偏差:代码给父级#home元素绑定了支持冒泡的mouseover、mouseout事件。
  • 因为代码提前把完整单词拆成了若干个仅包裹单个字符的<span>节点,当光标在单词内部从一个字符挪到相邻字符时,离开前一个字符触发的mouseout、进入新字符触发的mouseover,都会沿着DOM树冒泡到父元素#home上,直接触发绑定在父元素上的clear重置逻辑和highlight启动逻辑。
  • 这套事件逻辑不会校验光标是否真的离开了整个单词的整体边界,只要在字符间移动,就会反复清空已完成的高亮进度、重启逐字动画的定时器,最终表现为动画被不断重置。
  • 代码还存在附加隐患:原有的highlight函数没有做前置判断,只要触发就直接新开定时器,一旦事件被反复触发,会叠加多个同时运行的计时实例,进一步打乱逐字高亮的节奏。
修复方案

具体调整点

  • 替换为符合边界判断要求的事件类型:将容易因内部元素移动误触发的mouseover、mouseout,替换为不参与冒泡的mouseenter、mouseleave。这两类事件的触发逻辑是:只有光标真正进入绑定元素的整体可视区域、或是完全离开绑定元素的整体可视区域时才会触发,光标在内部的单个字符span之间移动时,不会向上冒泡触发父元素的对应事件。
  • 补充定时器防重复逻辑:每次启动逐字高亮的新定时器前,先清空上一次残留的计时实例,避免多定时器并行导致的动画错乱。

修正后代码

const home = document.querySelector("#home");
const homeText = home.textContent;
const splitHomeText = homeText.split("");

home.textContent = "";
for (let i = 0; i < splitHomeText.length; i++) {
  home.innerHTML += "<span>" + splitHomeText[i] + "</span>";
}

let char = 0;
let timer = null;
// 替换为边界触发的非冒泡事件
home.addEventListener("mouseenter", highlight);
home.addEventListener("mouseleave", clear);

function highlight() {
  // 清除已存在的定时器,避免重复叠加
  if (timer) clearInterval(timer);
  timer = setInterval(onTickHome, 50);
}

function onTickHome() {
  const span = home.querySelectorAll("span")[char];
  span.classList.add("highlight");
  char++;
  if (char === splitHomeText.length) {
    complete();
    return;
  }
}

function complete() {
  char = 0;
  clearInterval(timer);
}

function clear() {
  char = 0;
  clearInterval(timer);
  for (let i = 0; i < splitHomeText.length; i++) {
    const span = home.querySelectorAll("span")[i];
    span.classList.remove("highlight");
  }
}

配套的CSS和HTML不需要调整,与原有代码保持一致即可。

调整后效果

  • 光标在单词内部的不同字符间移动时,不会打断正在推进的逐字高亮流程
  • 只有当光标完全移出整个单词的可视边界时,才会清空所有高亮状态、重置动画进度,等待下一次悬停触发。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.25 00:24:04