Kto-Blog
Published on

TOC组件bug记录

Authors
  • avatar
    Name
    Kto

Bug 总结

这是我在为自己博客添加目录组件的过程中碰到的一个BUG,刚好记录一下。

问题描述

  • 现象:在测试过程中,发现双击目录的展开/收缩按钮后,组件会进入无限循环的展开和折叠状态,导致页面卡顿,用户体验较差。
  • 根本原因
    1. onToggle 事件的频繁触发
      • 双击操作会触发多次 onToggle 事件,导致 isOpen 状态被频繁更新,组件状态不稳定。
    2. 原生行为与 React 状态的冲突
      • <details> 元素的 open 属性与 React 的 isOpen 状态未完全同步,导致状态更新不一致。
    3. 防抖逻辑不足
      • 原有的防抖逻辑未能有效阻止快速双击或频繁点击导致的状态更新。

防抖的处理方法

1. 阻止原生行为

  • 问题<details> 元素的 onToggle 事件会触发原生行为,导致状态更新与 React 状态不同步。
  • 解决方案
    • handleToggle 中使用 e.preventDefault(),阻止 <details> 元素的默认行为。
    • 完全由 React 控制状态更新,避免原生行为干扰。
const handleToggle = (e: React.SyntheticEvent<HTMLDetailsElement>) => {
  e.preventDefault() // 阻止原生行为
  // 其他逻辑
}

2. 增强防抖逻辑

  • 问题:快速双击或频繁点击会触发多次状态更新,导致组件进入无限循环。
  • 解决方案
    • 使用 setTimeout 实现防抖,延迟状态更新。
    • 使用 isTogglingRef 标记是否正在切换状态,避免在防抖时间内重复触发状态更新。
    • 在状态更新前清除之前的定时器,确保只有最后一次更新生效。
const timerRef = useRef<NodeJS.Timeout | null>(null) // 用于防抖定时器
const isTogglingRef = useRef<boolean>(false) // 用于标记是否正在切换状态

const handleToggle = (e: React.SyntheticEvent<HTMLDetailsElement>) => {
  e.preventDefault() // 阻止原生行为

  if (isTogglingRef.current) return // 如果正在切换状态,则直接返回
  isTogglingRef.current = true // 标记为正在切换状态

  if (timerRef.current) {
    clearTimeout(timerRef.current)
  }

  timerRef.current = setTimeout(() => {
    setIsOpen((prev) => {
      const newState = !prev
      console.log('Toggling isOpen:', newState) // 调试日志
      return newState
    })
    isTogglingRef.current = false // 重置标记
  }, 100) // 防抖时间设置为 100ms
}

3. 手动同步状态

  • 问题<details> 元素的 open 属性与 React 的 isOpen 状态可能不同步。
  • 解决方案
    • 使用 useEffect 手动同步 <details>open 属性与 React 的 isOpen 状态,确保两者保持一致。
useEffect(() => {
  if (detailsRef.current) {
    detailsRef.current.open = isOpen
  }
}, [isOpen])

4. 减少不必要的渲染

  • 问题:状态更新可能导致不必要的渲染,影响性能。
  • 解决方案
    • 使用 useMemo 缓存过滤后的目录列表 (filteredToc),避免因状态更新导致的额外渲染。
const filteredToc = useMemo(() => {
  return toc.filter(
    (heading) =>
      heading.depth >= fromHeading &&
      heading.depth <= toHeading &&
      (!excludeRegex || !excludeRegex.test(heading.value))
  )
}, [toc, fromHeading, toHeading, excludeRegex])

详细处理思路

1. 问题定位

  • 通过调试日志 (console.log) 发现,双击会触发多次 onToggle 事件,导致 isOpen 状态被频繁更新。
  • 进一步分析发现,<details> 元素的 open 属性与 React 的 isOpen 状态不同步,导致循环更新。
  • 测试过程中,使用快速双击和频繁点击操作复现了问题,确认了防抖逻辑的不足。

2. 解决方案设计

  • 阻止原生行为:确保状态更新由 React 完全控制,避免原生行为干扰。
  • 增强防抖逻辑:使用 setTimeoutisTogglingRef 标记,确保防抖逻辑能够有效阻止频繁的状态更新。
  • 手动同步状态:使用 useEffect 确保 <details>open 属性与 React 的 isOpen 状态完全同步。
  • 减少不必要的渲染:使用 useMemo 缓存过滤后的目录列表,优化性能。

3. 代码实现

  • handleToggle 中阻止原生行为,并实现防抖逻辑。
  • 使用 useEffect 手动同步 <details>open 属性。
  • 使用 useMemo 缓存过滤后的目录列表。

4. 测试与验证

  • 快速双击测试:确保双击不会导致无限循环,组件状态稳定。
  • 状态同步测试:检查 <details>open 属性是否与 React 的 isOpen 状态完全同步。
  • 性能测试:确保组件在频繁交互时不会出现性能问题,页面响应流畅。
  • 回归测试:验证修复后的代码是否影响其他功能,确保整体功能正常。

总结

通过以上方法,我成功解决了 双击导致无限循环 的问题,并增强了组件的健壮性。以下是关键点:

  1. 阻止原生行为:确保状态更新由 React 完全控制,避免原生行为干扰。
  2. 增强防抖逻辑:使用 setTimeoutisTogglingRef 标记,有效阻止频繁的状态更新。
  3. 手动同步状态:确保 <details>open 属性与 React 的 isOpen 状态完全同步。
  4. 减少不必要的渲染:优化性能,避免因状态更新导致的额外渲染。

我会在后续的测试中重点关注以下几点:

  • 边界测试:测试极端情况下的组件行为,例如快速多次点击、网络延迟等。
  • 兼容性测试:确保修复后的代码在不同浏览器和设备上表现一致。
  • 性能监控:通过性能分析工具监控组件的渲染性能,确保优化效果符合预期。