Poznajemy różne sposoby na tzw. autobind dekorator. Dalej zatem brniemy w TypeScripta i choć jest ciężko, nie poddajemy się. Do dzieła.
Ok, oto HTML:
<body>
<button id="btn">clickme</button>
<script src="./dist/autobind1.js" defer></script>
</body>
Ok, TS:
class ButtonHandler {
message = 'Hello world';
showMessage() {
console.log(this.message);
}
}
const btn1handler = new ButtonHandler();
let btn1 = <HTMLButtonElement>document.querySelector("#btn");
btn1.addEventListener("click", btn1handler.showMessage);
I dla przypomnienia jak to po kompilacji będzie wyglądać, bo mi osobiście zawsze dziwnie te pola z wartościami poza konstruktorem wyglądają, zawsze myślę, że to niby statyczne są:
class ButtonHandler {
constructor() {
this.message = 'Hello world';
}
showMessage() {
console.log(this.message);
}
}
const btn1handler = new ButtonHandler();
let btn1 = document.querySelector("#btn");
btn1.addEventListener("click", btn1handler.showMessage);
Statyczny to by tak wyglądał:
class ButtonHandler {
static message = 'Hello world';
}
A po kompilacji tak (nie wiem, może tylko mi się to zawsze myli):
class ButtonHandler {
}
ButtonHandler.message = 'Hello world';
Ok, nieważne. Teraz chodzi o to, że message to będzie undefined bo this będzie wskazywać na button, nie obiekt ButtonHandler. Można to rozwiązać:
class ButtonHandler {
message = 'Hello world';
// @autobind
showMessage() {
console.log(this.message);
}
}
const btn1handler = new ButtonHandler();
let btn1 = <HTMLButtonElement>document.querySelector("#btn");
btn1.addEventListener("click", btn1handler.showMessage.bind(btn1handler));
To już działa, ale my chcemy to robić bez bind. Chcemy autobind, taki dekorator, i on ma to robić za nas. Poznamy dwa sposoby, znalezione w internecie.
Pierwszy sposób:
function Autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const adjDescriptor: PropertyDescriptor = {
configurable: true,
enumerable: false,
get() {
const boundFn = originalMethod.bind(this);
return boundFn;
}
};
return adjDescriptor;
}
Od razu dodam, nigdy się nie miesza value i writable razem z get i set. Configurable i enumerable mogą być wszędzie, ale set i get (którekolwiek z nich) wykluczają value i writable (którekolwiek z nich).
Dziwne, trudne, zwłaszcza, że tutaj jeszcze mieszamy jedne z drugimi, tak nam się może wydawać, bo originalMethod bierzemy po value, ale zwracamy coś, co ma gettera.
Ok, zobaczmy jak to działa:
class ButtonHandler {
message = 'Hello world';
@Autobind
showMessage() {
console.log(this.message);
}
}
const btn1handler = new ButtonHandler();
let btn1 = <HTMLButtonElement>document.querySelector("#btn");
btn1.addEventListener("click", btn1handler.showMessage.bind(btn1handler));
Działa tak, że jest ok. A teraz taki psikus:
class ButtonHandler {
message = 'Hello world';
@Autobind
showMessage() {
console.log(this.message);
}
}
const btn1handler = new ButtonHandler();
let btn1 = <HTMLButtonElement>document.querySelector("#btn");
btn1.addEventListener("click", btn1handler.showMessage.bind(btn1handler));
ButtonHandler.prototype.showMessage = function(){
console.log(`${this.message.toUpperCase()}`);
}
Teraz co prawda stara metoda dalej działa i binding też jest ok, ale nowa nie została nadpisana. Bo settera nie mamy. Ach, działa pewnie tylko dlatego, że jest definiowana po event listenerze, gdyby dać przed to byśmy nic nie mieli.
Ok, ściągnijmy sobie z lepszej biblioteki kod dekoratora autobind:
const IDENTIFIER = `@typed-decorators/autobind`;
function autobind<F extends (...args: Array<any>) => any>(
_target: any,
name: string,
descriptor: TypedPropertyDescriptor<F>,
): TypedPropertyDescriptor<F> {
const { enumerable, configurable, value } = descriptor;
const boundMethod = Symbol(`${IDENTIFIER}/${name}`);
return {
enumerable,
configurable,
get(this: { [boundMethod]: any }) {
return this[boundMethod] || (this[boundMethod] = value!.bind(this));
},
set(value: F) {
Object.defineProperty(this, name, {
writable: true,
enumerable: true,
configurable: true,
value,
});
},
};
}
Ok, spróbujmy:
class ButtonHandler {
message = 'Hello world';
@autobind
showMessage() {
console.log(this.message);
}
}
const btn1handler = new ButtonHandler();
let btn1 = <HTMLButtonElement>document.querySelector("#btn");
ButtonHandler.prototype.showMessage = function(){
console.log(`${this.message.toUpperCase()}`);
}
btn1.addEventListener("click", btn1handler.showMessage);
No jest błąd, ale nadpisać się dało. Takie coś by na pewno dało radę:
const btn1handler = new ButtonHandler();
let btn1 = <HTMLButtonElement>document.querySelector("#btn");
ButtonHandler.prototype.showMessage = function(){
console.log(`HELLO WORLD`);
}
btn1.addEventListener("click", btn1handler.showMessage);
Pytanie po co ktoś miałby to robić (nadpisywać metodę). To jest głupie. Natomiast rzućmy okiem na ten pierwszy autobind i porównajmy z drugim:
function Autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const adjDescriptor: PropertyDescriptor = {
configurable: true,
enumerable: false,
get() {
const boundFn = originalMethod.bind(this);
console.log("getter called" + Math.random());
return boundFn;
}
};
return adjDescriptor;
}
Te console logi żeby łatwiej było zrozumieć ile razy getter chodzi (random aby się wyświetlały jeden pod drugim). Ok, dodamy sobie jeszcze drugi button i taki kod:
class ButtonHandler {
message = 'Hello world';
@Autobind
showMessage() {
console.log(this.message);
}
}
const btn1handler = new ButtonHandler();
let btn1 = <HTMLButtonElement>document.querySelector("#btn");
let btn2 = <HTMLButtonElement>document.querySelector("#btn2");
btn1.addEventListener("click", btn1handler.showMessage);
btn2.addEventListener("click", btn1handler.showMessage);
btn1.removeEventListener("click", btn1handler.showMessage);
btn2.removeEventListener("click", btn1handler.showMessage);
Po pierwsze 4 gettery (to znaczy to nie powinno dziwić) po drugie nic się nie usunęło. Czemu? Bo to jest ciągle nowa funkcja. Ok, a autobind:
const IDENTIFIER = `@typed-decorators/autobind`;
function autobind<F extends (...args: Array<any>) => any>(
_target: any,
name: string,
descriptor: TypedPropertyDescriptor<F>,
): TypedPropertyDescriptor<F> {
const { enumerable, configurable, value } = descriptor;
const boundMethod = Symbol(`${IDENTIFIER}/${name}`);
return {
enumerable,
configurable,
get(this: { [boundMethod]: any }) {
console.log("getter called" + Math.random());
return this[boundMethod] || (this[boundMethod] = value!.bind(this));
},
set(value: F) {
Object.defineProperty(this, name, {
writable: true,
enumerable: true,
configurable: true,
value,
});
},
};
}
class ButtonHandler {
message = 'Hello world';
@autobind
showMessage() {
console.log(this.message);
}
}
const btn1handler = new ButtonHandler();
let btn1 = <HTMLButtonElement>document.querySelector("#btn");
let btn2 = <HTMLButtonElement>document.querySelector("#btn2");
btn1.addEventListener("click", btn1handler.showMessage);
btn2.addEventListener("click", btn1handler.showMessage);
btn1.removeEventListener("click", btn1handler.showMessage);
btn2.removeEventListener("click", btn1handler.showMessage);
Też 4 gettery, ale tutaj mamy tę samą funkcję. Symbol to załatwia. Dlatego da się odpiąć listenera.
Generalnie to są wszystko trudne rzeczy plus bardziej my będziemy z dekoratorów korzystać, niż je pisać, ale pięknie to pokazuje, ile rzeczy należy brać pod uwagę pisząc w TS oraz fakt, że nie każdy kod, który pozornie działa, będzie działać bez problemu.
Można nawet pomyśleć, że ten TS i 'bezpieczeństwo’ jakie on zapewnia, jest dość iluzoryczne. Na pewno jeżeli chcemy mieć zysk z TSa, musimy go dobrze znać, czysty JS również. Inaczej możemy wręcz narobić sobie niepotrzebnych problemów.