考虑一个包含触发模式打开的按钮的网页:
<dialog></dialog>
<button>Open modal</button>
const trigger = document.querySelector(`button`);
const wrapper = document.querySelector(`dialog`);
trigger.addEventListener(`click`, () => {
wrapper.replaceChildren(new Modal());
wrapper.showModal();
});
模态是自定义元素:
class Modal extends HTMLElement {
connectedCallback() {
this.innerHTML = `<button onclick="this.closest('dialog').close()">Close modal</button>`;
this.closest(`dialog`).addEventListener(`close`, () => {
console.log(`Modal closed`);
});
}
}
customElements.define(`app-modal`, Modal);
这是一个演示:https://codepen.io/RobertAKARobin/pen/OJGJQro
我期望的是每次单击
Close modal
按钮时,控制台都会记录 'Modal closed'
。
实际发生的是控制台记录
'Modal closed'
一次,每次到目前为止模式已经关闭。因此,单击 1 时,它会记录消息 1 次,单击 2 时,它会记录 2 次,单击 3 时,它会记录 3 次,单击 4 时,它会记录 4 次,依此类推。
这表明 Modal 实例在与 DOM 断开连接时不会被自动垃圾回收。我怀疑这是因为事件监听器。但是,侦听器函数本身不包含对 Modal 实例的任何引用(通过
this
除外)。
设置 Modal 的正确方法是什么,这样它就不会阻止自身被垃圾收集?
问题来自于你的
Modal
自定义元素,与GC无关。
您在
connectedCallback
中添加事件监听器,但永远不要删除它。
对话框不会离开 DOM,之前的自定义元素还没有被垃圾回收,因此它们的侦听器仍然处于活动状态。
如果你正确地断开了自定义元素与 DOM 的连接,你可以使用这个:
class Modal extends HTMLElement {
constructor() {
super();
this.onDialogClose = this.onDialogClose.bind(this);
}
connectedCallback() {
this.innerHTML = `<button onclick="this.closest('dialog').close()">Close modal</button>`;
this.closest('dialog').addEventListener('close', this.onDialogClose);
}
disconnectedCallback() {
this.closest('dialog').removeEventListener('close', this.onDialogClose);
}
onDialogClose() {
console.log('Modal closed');
}
}
customElements.define('app-modal', Modal);
事件处理程序不会自动从跨打开/关闭周期停留的宿主元素(对话框)中删除。因此,即使从断开连接的自定义元素中,每个关闭事件都会调用重复的事件处理程序(侦听器仍保留)。
我认为可以用更好的方式完成,但是每当
addEventListener
中有 connectedCallback
时,removeEventListener
中应该有相应的 disconnectedCallback
调用。
因此,来自
this.listener
的监听器(例如 this.host
)和主机引用(例如 connectedCallback
)必须以某种方式保留以便稍后释放。
请查看应用了修复程序的分叉演示:https://codepen.io/Ciunkos/pen/PogoRGV