- Published on
TOC组件bug记录
- Authors
- Name
- Kto
Bug 总结
这是我在为自己博客添加目录组件的过程中碰到的一个BUG,刚好记录一下。
问题描述
- 现象:在测试过程中,发现双击目录的展开/收缩按钮后,组件会进入无限循环的展开和折叠状态,导致页面卡顿,用户体验较差。
- 根本原因:
onToggle
事件的频繁触发:- 双击操作会触发多次
onToggle
事件,导致isOpen
状态被频繁更新,组件状态不稳定。
- 双击操作会触发多次
- 原生行为与 React 状态的冲突:
<details>
元素的open
属性与 React 的isOpen
状态未完全同步,导致状态更新不一致。
- 防抖逻辑不足:
- 原有的防抖逻辑未能有效阻止快速双击或频繁点击导致的状态更新。
防抖的处理方法
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 完全控制,避免原生行为干扰。
- 增强防抖逻辑:使用
setTimeout
和isTogglingRef
标记,确保防抖逻辑能够有效阻止频繁的状态更新。 - 手动同步状态:使用
useEffect
确保<details>
的open
属性与 React 的isOpen
状态完全同步。 - 减少不必要的渲染:使用
useMemo
缓存过滤后的目录列表,优化性能。
3. 代码实现
- 在
handleToggle
中阻止原生行为,并实现防抖逻辑。 - 使用
useEffect
手动同步<details>
的open
属性。 - 使用
useMemo
缓存过滤后的目录列表。
4. 测试与验证
- 快速双击测试:确保双击不会导致无限循环,组件状态稳定。
- 状态同步测试:检查
<details>
的open
属性是否与 React 的isOpen
状态完全同步。 - 性能测试:确保组件在频繁交互时不会出现性能问题,页面响应流畅。
- 回归测试:验证修复后的代码是否影响其他功能,确保整体功能正常。
总结
通过以上方法,我成功解决了 双击导致无限循环 的问题,并增强了组件的健壮性。以下是关键点:
- 阻止原生行为:确保状态更新由 React 完全控制,避免原生行为干扰。
- 增强防抖逻辑:使用
setTimeout
和isTogglingRef
标记,有效阻止频繁的状态更新。 - 手动同步状态:确保
<details>
的open
属性与 React 的isOpen
状态完全同步。 - 减少不必要的渲染:优化性能,避免因状态更新导致的额外渲染。
我会在后续的测试中重点关注以下几点:
- 边界测试:测试极端情况下的组件行为,例如快速多次点击、网络延迟等。
- 兼容性测试:确保修复后的代码在不同浏览器和设备上表现一致。
- 性能监控:通过性能分析工具监控组件的渲染性能,确保优化效果符合预期。