fix: 共享悬浮容器的监听器不再随面板重建而无界累积 - #67
Merged
Merged
Conversation
`tipLayer` 是长期存活的 body 级单例(只在插件销毁时 remove),而气泡标签
(attachBubbleTag)与回复芯片(makeReplyChip)会被反复重建 —— 消息重渲染、语言
切换、切会话回来都会让它们换一代。改动前每个面板都在自己的 mouseenter 里往
`tipLayer` 追加一对监听器,却从不移除:
tag.addEventListener('mouseleave', bubbleHide)
tipLayer.addEventListener('mouseenter', bubbleKeep) // ← 每代 +1
tipLayer.addEventListener('mouseleave', bubbleHide) // ← 每代 +1
回复芯片同理(keep / hide)。于是每次重建净增两个闭包,它们持有已废弃的 grace
定时器与已被移出 DOM 的触发元素;关闭决策不再由「当前面板」决定,而是由历史上
任意一代的定时器决定 —— 这既让面板的关闭时机变得不可预测,也随会话变长持续泄漏。
改动:
- 共享容器上只注册两处固定监听器 sharedTipMouseEnter / sharedTipMouseLeave;
- 新增 `tipActiveHide` 指针:由面板在 mouseenter 时登记 `{ keep, hide }`,
共享监听器按它派发;
- 新增 releaseActiveTip():面板真正关闭时解除登记(三处宽限到点的关闭都调用),
避免残留钩子仍被派发;
- 移除三类面板中各自的 tipLayer.addEventListener。
对外行为保持不变:指针进入面板仍会取消关闭,离开仍会按各自的 250ms 宽限关闭。
回归测试 test/shared-listeners.test.mjs(8 条):结构断言「tipLayer 上恰好两处
监听器」+ 把真实的 sharedTipMouseEnter / sharedTipMouseLeave / releaseActiveTip
抽到 vm 沙箱里按行为验证派发目标、面板换代、无登记空操作、解除登记后不再派发。
同一套用例跑上游原版 client.js:8/8 失败;本分支 8/8 通过。
注:本 PR 与 omdsh-dev#65、以及「跨间隙保活」那个 PR 均正交。omdsh-dev#65 引入归属模型解决「宿主
layout mutation 用 updateChip 抹掉面板」与「定时器跨面板误杀」;本 PR 只处理
「监听器挂在共享容器上、随面板重建累积」这一条。
This was referenced Sep 24, 2026
Merged
Collaborator
|
感谢 @Ryuu-64 的精准定位——tipLayer 作为 body 级单例,被每代重建的面板追加监听器而无界累积,这个泄漏在长会话下确实会持续恶化。 情况说明:你的 fork 未开放维护者推送,无法把合并提交直接推上你的分支,因此我们把「你的原始提交 + 与 main 的合并解决」一起快进推入了 main(3ab6530)——你的提交 合并中做的适配:与 #65 引入的 tipOwner 归属清空机制正交并存——tipActiveHide 管监听器泄漏(每次重建不再净增闭包),tipOwner 管「无归属清空误杀正在观看的面板」;所有 presentTip/clearTip 归属配对(chipLayer/tag/reply chip)保持不变,releaseActiveTip 挂在每个真实关闭路径上。测试 41/41 全绿,CHANGELOG 已注明方案来自你。 接下来会以同样方式处理你的 #66(实时指针判定),它会在本次改动之上 rebase。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
现象
长会话、消息反复重渲染(流式结束后重新装饰、切换语言、切会话回来)之后,悬浮面板的关闭行为变得不可预测:有时刚出现就被关掉,有时关不掉。
根因
tipLayer是长期存活的 body 级单例(只在插件销毁时remove()),而气泡标签(attachBubbleTag)与回复芯片(makeReplyChip)是会被反复重建的——消息重渲染、语言切换、切会话回来都会让它们换一代。但改动前,每个面板都在自己的
mouseenter里往共享容器追加一对监听器,却从不移除:于是面板每次重建就净增两个订阅者,它们闭包持有已废弃的 grace 定时器与已被移出 DOM 的触发元素:
改动
共享容器上只保留两处固定监听器,通过一个「当前面板的关闭钩子」指针派发:
同时删除三类面板中各自的
tipLayer.addEventListener。为什么需要 releaseActiveTip
如果只在
mouseenter登记而从不解除,tipActiveHide会一直指向最后一代面板:该面板被移出 DOM 后,指针再划过共享容器仍会去调用它的hide()。关闭时解除登记,共享监听器就退化为空操作。对外行为不变
指针进入面板仍然取消关闭;离开仍按各面板自己的 250ms 宽限关闭。本 PR 不改变清理策略、不引入归属模型。
回归测试
新增
test/shared-listeners.test.mjs(8 条)。结构断言 + 把client.js里真实的sharedTipMouseEnter/sharedTipMouseLeave/releaseActiveTip抽到node:vm沙箱里按行为验证:sharedTipMouseEnter/sharedTipMouseLeavetipLayer.addEventListener('mouseenter', bubbleKeep|keep|cancelHide)等已彻底消失tipActiveHide = { keep, hide }releaseActiveTip()定义且三处关闭都调用mouseenter→keep(),mouseleave→hide()反向验证——同一套用例跑上游原版
client.js:8/8 失败;本分支 8/8 通过。与其它 PR 的关系
正交,可各自独立合并。
updateChip()抹掉正在观看的面板」与「250ms 定时器跨面板误杀」;DSH Desktop 2.0.11 / 内核 0.1.5-rc.2 / Windows 11 实测:改动前后功能表现一致,差别在于共享容器上的监听器数量不再随重渲染增长。