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 的内部实现原理

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);
}