Svelte 取消监听仍触发
svelte/events 的取消函数早于微任务注册执行,旧监听可能重新挂上并保留卸载组件。核对 5.57.1 修复与 effect 清理条件。
组件已经执行事件清理,页面上的 wheel 或 pointerdown 回调却还会触发。监听器闭包继续引用卸载后的组件,长时间导航后还可能留下越来越多的旧对象。
先检查是否使用了 svelte/events 的 on(),以及取消监听是否发生在同一轮更新中、早于延迟注册的微任务。Svelte 5.56.10 存在这一时间窗口;正式版 5.57.1 修正了它,5.57.2 保留相应处理。
清理执行过,不代表监听已经取消
Svelte 对 pointer*、touch* 和 wheel 事件会延迟到微任务再调用 addEventListener。这一安排是为了解决特定浏览器中克隆、尚未连接的元素监听问题。
旧 on() 返回的取消函数却同步调用 removeEventListener。如果它在注册微任务之前执行,此时监听器尚未挂上,删除就没有目标。之后微任务仍继续注册,已经用过的取消函数也不再负责清掉它。
这一顺序不需要应用刻意把订阅和取消写在相邻两行。$effect 重新运行时,会先清理前一轮再执行新一轮;挂载期间的依赖变化也可能在微任务排空前触发它。于是旧一轮和新一轮监听器最终都挂上,应用只保留了新一轮的清理关系。
对照已有的 effect 清理路径
检查注册位置是否把 on() 返回的函数交给 effect 清理。下面只展示已有处理函数的接线方式,handleWheel 应由应用定义,并保留它原有的事件语义:
import { on } from 'svelte/events';
$effect(() => {
const off = on(document, 'wheel', handleWheel, { passive: false });
return off;
});
这不是让所有页面都注册非 passive 的滚轮监听。仅当原有逻辑确实需要阻止默认滚动时才保留该选项;没有这种需要的项目,应继续使用自己的正常选项。
如果应用根本没有保存取消函数,或者 effect 清理没有返回它,先修复应用的生命周期。旧框架时间窗口与缺少清理是两个问题,升级不能替应用补上未写的取消逻辑。
正式修复同时处理两种时机
修正后的取消函数先标记目标处理器已经移除,再调用 removeEventListener。尚未执行的注册微任务检查这个标记,发现取消后便不会挂上监听;如果注册已经完成,原来的删除路径仍照常工作。
正式实现也处理了相关模板事件的销毁路径,包括 document、window、body 和媒体元素对应的清理。它没有简单取消全部事件延迟,因此原有注册时机和事件委托顺序仍得以保留。
沿项目兼容路径升级 Svelte 后,重新构建实际客户端资源。不要为了躲开这一窗口,把清理函数改为永远不运行,或把所有事件换成裸 addEventListener 却丢掉原来的委托顺序和移除关系。
验收早取消与正常取消
在受控环境中保留原来的挂载更新条件,确认前一轮 effect 的回调不会在取消之后重新生效;新一轮只保留需要的监听。再核对监听已挂上之后的正常取消,以及组件卸载后的事件行为。
如果排查的是内存增长,继续查看真正保留组件的引用链,确认旧事件闭包不再持有卸载子树。不能仅凭清理日志打印过,或垃圾回收后某一次内存读数下降,就认定这个窗口已经关闭。


