blog
blog copied to clipboard
事件绑定与类继承结合时的最佳实践
事件绑定和类继承都是很常用的东西, 当它俩结合起来时, 可能并不会像你所想的那样工作
来看一个最简单的例子, 在构造函数中绑定 click 事件, 点击后打印 "click"
和 this.a
在该例中 this.a
会打印什么呢? 会打印 undefined
, 因为 handleClick
的 this 指向是 button dom 对象, dom 对象没有 a 属性
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<button id="click">click</button>
<script>
class Base {
a = 1;
constructor() {
document.querySelector("#click").addEventListener("click", this.handleClick);
}
handleClick() {
console.log("click", this.a); // click undefined
}
}
new Base();
</script>
</body>
</html>
首先, 可以用箭头函数来解决 this 指向问题, 在 react 中这种写法很常见, 这没什么问题
handleClick = () => {
console.log("click", this.a); // click 1
};
但是, 当与类继承相结合时会怎样呢? 如下的例子中, 派生类继承基类并重载 handleClick
方法
点击后会并不会输出 "click2"
, 因为基类的 handleClick
是定义在实例属性上, 而派生类的 handleClick
是定义在派生类的原型链上, 实例属性访问优先级大于原型链, 所以根本没执行到派生类的 handleClick
class Base {
a = 1;
constructor() {
document.querySelector("#click").addEventListener("click", this.handleClick);
}
handleClick = () => {
console.log("click", this.a); // click 1
};
}
class Derived extends Base {
handleClick() {
super.handleClick();
console.log("click2", this.a); // not run
}
}
new Derived();
尝试通过原型链直接调用派生类的 handleClick
, 注意! 由于是直接调用的, super.handleClick()
不可用需要注释掉
会输出 click2 1
, 但是不会调到基类方法
class Base {
a = 1;
handleClick = () => {
console.log("click", this.a); // not run
};
}
class Derived extends Base {
handleClick() {
// super.handleClick();
console.log("click2", this.a); // click2 1
}
}
const ins = new Derived();
Derived.prototype.handleClick.call(ins);
修改一下, 将基类改为普通函数, 并在绑定事件时 bind this, 这就是我们所期望的效果了
class Base {
a = 1;
constructor() {
document.querySelector("#click").addEventListener("click", this.handleClick.bind(this));
}
handleClick() {
console.log("click", this.a); // click 1
}
}
class Derived extends Base {
handleClick() {
super.handleClick();
console.log("click2", this.a); // click2 1
}
}
接下来, 我们增加一个需求, 新增一个按钮用来取消事件订阅
如下所示, 点击 unsubscribe 按钮后调用 removeEventListener 取消事件订阅, 但是并不起作用(包括注释那行)
为什么呢? 因为订阅和取消订阅的并不是同一个方法, 订阅时的 bind 调用会返回一个全新函数, 由于没有保存该函数引用, 调用 removeEventListener 也就无法将其取消订阅
怎么解决呢?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<button id="click">click</button>
<br />
<button id="unsubscribe">unsubscribe</button>
<script>
class Base {
a = 1;
constructor() {
document.querySelector("#click").addEventListener("click", this.handleClick.bind(this));
document.querySelector("#unsubscribe").addEventListener("click", () => {
// 无法取消订阅
document.querySelector("#click").removeEventListener("click", this.handleClick.bind(this));
// document.querySelector("#click").removeEventListener("click", this.handleClick);
});
}
handleClick() {
console.log("click", this.a);
}
}
class Derived extends Base {
handleClick() {
super.handleClick();
console.log("click2", this.a);
}
}
new Derived();
</script>
</body>
</html>
只要将 bind 后的实例保存下来即可, 这样就能确保订阅和取消订阅的是同一方法了, 完美达成期望
constructor() {
this.handleClick = this.handleClick.bind(this); // 保存 bind 后方法
document.querySelector('#click').addEventListener('click', this.handleClick);
document.querySelector('#unsubscribe').addEventListener('click', () => {
// 可以取消订阅
document.querySelector('#click').removeEventListener('click', this.handleClick);
});
}
还可以用
class Base {
a = 1;
constructor() {
document.querySelector("#click").addEventListener("click", this.handleClick.bind(this));
}
@autobind // 装饰器
handleClick() {
console.log("click", this.a); // click 1
}
}
class Derived extends Base {
@autobind
handleClick() {
super.handleClick();
console.log("click2", this.a); // click2 1
}
}
总结
- 用箭头函数解决 this 绑定问题时, 该方法(其实是属性)无法被重载
- bind 调用会返回一个全新方法, 无法用其原方法取消事件订阅