typescript之绑定和解绑事件
如果在类中,你函数中使用了 this,必须像这样绑定
private boundDemoCallback: () => void;
demoCallback(a: any,b: any, c: any) {
console.log(a,b,c, 1111);
}
demoListen () {
//const btn = this.app.workspace.containerEl.find(".demo1")
const btn = document.querySelector(".demo1")
this.boundDemoCallback = this.demoCallback.bind(this, 1, 2, 3);
this.registerDomEvent(btn as HTMLElement, 'click', this.boundDemoCallback)
}
demoRemoveListen() {
const btn = document.querySelector(".demo1")
btn?.removeEventListener('click', this.boundDemoCallback);
}
这里借助了 this.boundDemoCallback 作为中转函数并传递参数,如果直接写 this.demoCallback.bind(this)是无法解绑的。
注意: 这里的 this.registerDomEvent
是 obsidian 的写法,相当于 btn.addEventListener('click', this.boundDemoCallback);
在JavaScript中,bind
方法会创建一个新的函数实例,这个新的函数会拥有与原函数相同的主体,但它的this
上下文会被绑定到bind
方法的第一个参数所指定的对象。
当你在事件监听器中使用this.demoCallback.bind(this)
时,实际上是创建了一个新的函数实例,并将其作为监听器附加到DOM元素上。在尝试解绑事件监听器时,你使用的依然是this.demoCallback
这个原始函数引用,而不是之前通过bind
创建并添加到事件监听链的那个新函数实例。
因此,当执行removeEventListener
时,因为比较的是函数引用的地址,而非函数内容,所以无法匹配到之前通过bind
创建并添加的事件监听器,从而无法解绑。
正确的做法是在添加事件监听器时保存 bind
之后的新函数引用,然后在解绑时使用这个引用:
在 JavaScript 中,bind
方法会创建一个新的函数实例,这个新的函数会拥有与原函数相同的主体,但它的 this
上下文会被绑定到 bind
方法的第一个参数所指定的对象。
当你在事件监听器中使用 this.demoCallback.bind(this)
时,实际上是创建了一个新的函数实例,并将其作为监听器附加到 DOM 元素上。在尝试解绑事件监听器时,你使用的依然是 this.demoCallback
这个原始函数引用,而不是之前通过 bind
创建并添加到事件监听链的那个新函数实例。
因此,当执行 removeEventListener
时,因为比较的是函数引用的地址,而非函数内容,所以无法匹配到之前通过 bind
创建并添加的事件监听器,从而无法解绑。
正确的做法是在添加事件监听器时保存 bind
之后的新函数引用,然后在解绑时使用这个引用:
// 添加事件监听器时保存绑定过的函数引用
this.boundDemoCallback = this.demoCallback.bind(this);
btn?.addEventListener('click', this.boundDemoCallback);
// 解绑事件监听器时使用保存的引用
btn?.removeEventListener('click', this.boundDemoCallback);
这样,添加和移除的就是同一个函数实例,从而可以正确地解绑事件监听器。
bind
方法的内部实现原理确实类似于创建了一个新的函数,该函数在调用时会维持指定的上下文(this
值),并可选地预设一些参数。但实际的实现要比您给出的简写形式稍微复杂一些,因为它还需要处理额外的细节,比如保留原始函数的原型链、不可枚举属性等。
概念上,bind
生成的函数可能近似于:
var boundFunc = function(...args) {
// 把传递给新函数的参数和之前bind时预设的参数合并
var combinedArgs = [...presetArgs, ...args];
// 设置正确的this上下文
var context = thisArg || window; // 根据调用情况确定this值,默认为window/global
// 调用原始函数
return this.demoCallback.apply(context, combinedArgs);
};
这里只是简化了 bind
实现的一个大概思路,真实的 bind
方法还会处理更多的边界情况和兼容性问题。而在事件监听的场景下,重点在于 bind
会返回一个新的函数引用,所以在解绑时需要使用这个返回的新函数引用。
但,这个箭头函数必须在绑定和解绑函数中都能访问到才行,如果在类中,要么赋值给属性,要么赋值给全局变量,而且要注意 this 指向问题。
比如:
private demoCallbackWrapper: () => void;
demoCallback(a,b,c) {
console.log(a,b,c, 1111);
}
demoListen () {
const btn = document.querySelector(".side-dock-settings > .side-dock-ribbon-action:nth-child(3)")
this.demoCallbackWrapper = () => {
this.demoCallback(1, 2, 3);
};
this.registerDomEvent(btn as HTMLElement, 'click', this.demoCallbackWrapper)
// setTimeout(() => {
// console.log("removed event");
// btn?.removeEventListener('click', demoCallbackWrapper);
// }, 10000);
}
demoRemoveListen() {
const btn = document.querySelector(".side-dock-settings > .side-dock-ribbon-action:nth-child(3)")
btn?.removeEventListener('click', this.demoCallbackWrapper);
}
再比如:
demoCallback(a,b,c) {
console.log(a,b,c, this.settings.searchUrl, 1111);
}
demoListen () {
const btn = document.querySelector(".side-dock-settings > .side-dock-ribbon-action:nth-child(3)")
window.demoCallbackWrapper = function() => {
console.log('i am in');
this.demoCallback(1, 2, 3);
};
this.registerDomEvent(btn as HTMLElement, 'click', window.demoCallbackWrapper)
}
demoRemoveListen() {
const btn = document.querySelector(".side-dock-settings > .side-dock-ribbon-action:nth-child(3)")
btn?.removeEventListener('click', window.demoCallbackWrapper);
}
再比如:
demoCallback(a,b,c) {
console.log(a,b,c, 1111);
}
demoListen () {
const btn = document.querySelector(".side-dock-settings > .side-dock-ribbon-action:nth-child(3)")
//this.boundDemoCallback = this.demoCallback.bind(this, 1, 2, 3);
const demoCallbackWrapper = () => {
this.demoCallback(1, 2, 3);
};
this.registerDomEvent(btn as HTMLElement, 'click', demoCallbackWrapper)
setTimeout(() => {
console.log("removed event");
btn?.removeEventListener('click', demoCallbackWrapper);
}, 10000);
}