Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

事件绑定与类继承结合时的最佳实践 #17

Open
yinxin630 opened this issue Jun 17, 2019 · 0 comments
Open

事件绑定与类继承结合时的最佳实践 #17

yinxin630 opened this issue Jun 17, 2019 · 0 comments

Comments

@yinxin630
Copy link
Owner

事件绑定和类继承都是很常用的东西, 当它俩结合起来时, 可能并不会像你所想的那样工作

来看一个最简单的例子, 在构造函数中绑定 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);
    });
}

还可以用 (https://www.npmjs.com/package/autobind-decorator) 装饰器自动完成 bind 操作

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

总结

  1. 用箭头函数解决 this 绑定问题时, 该方法(其实是属性)无法被重载
  2. bind 调用会返回一个全新方法, 无法用其原方法取消事件订阅
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant