feat: performance improvements, bindings to refs can be unsubbed

This commit is contained in:
Daniel Ledda
2022-04-03 12:04:52 +02:00
parent f31cc87d20
commit bfb2ae8758
7 changed files with 144 additions and 65 deletions

View File

@@ -1,17 +1,21 @@
import ISubscriber, {LEvent} from "./Subscriber"; import ISubscriber, {LEvent} from "./Subscriber";
class Subscription<EventType extends LEvent, PublisherType> implements ISubscription { class PublisherSubscription<EventType extends LEvent> implements ISubscription {
private subscriber: ISubscriber<EventType>; private subscriber: ISubscriber<EventType>;
private readonly eventTypes: EventType[]; private readonly eventTypes: EventType[];
private publisher: Publisher<EventType, PublisherType>; private unbindCallback?: () => void;
constructor(publisher: Publisher<EventType, PublisherType>, subscriber: ISubscriber<EventType>, eventTypes: EventType[]) {
constructor(subscriber: ISubscriber<EventType>, eventTypes: EventType[]) {
this.subscriber = subscriber; this.subscriber = subscriber;
this.publisher = publisher;
this.eventTypes = eventTypes; this.eventTypes = eventTypes;
} }
setUnbind(unbind: () => void): void {
this.unbindCallback = unbind;
}
unbind(): void { unbind(): void {
this.publisher.unbind(this); this.unbindCallback?.();
} }
getEventTypes(): EventType[] { getEventTypes(): EventType[] {
@@ -48,10 +52,12 @@ export class Publisher<EventType extends LEvent, PublisherType> implements IPubl
for (const key of eventTypes) { for (const key of eventTypes) {
this.getSubscribers(key).push(subscriber); this.getSubscribers(key).push(subscriber);
} }
return new Subscription(this, subscriber, eventTypes); const sub = new PublisherSubscription(subscriber, eventTypes);
sub.setUnbind(() => this.unbind(sub));
return sub;
} }
unbind(subscription: Subscription<EventType, PublisherType>): void { private unbind(subscription: PublisherSubscription<EventType>): void {
for (const key of subscription.getEventTypes()) { for (const key of subscription.getEventTypes()) {
const subs = this.getSubscribers(key); const subs = this.getSubscribers(key);
subs.splice(subs.indexOf(subscription.getSubscriber()), 1); subs.splice(subs.indexOf(subscription.getSubscriber()), 1);

View File

@@ -1,4 +1,22 @@
export default class Ref<T extends { toString(): string } | string | null = string> { import {ISubscription} from "@/Publisher";
class RefSubscription implements ISubscription {
private unbindCallback?: () => void;
constructor(unbindCallback: () => void) {
this.unbindCallback = unbindCallback;
}
unbind(): void {
this.unbindCallback?.();
}
}
interface Stringable {
toString(): string;
}
export default class Ref<T extends { toString(): string } | string | null = Stringable> {
private watchers: Array<(newVal: T) => void> | null = null; private watchers: Array<(newVal: T) => void> | null = null;
private value: T; private value: T;
private asString?: string; private asString?: string;
@@ -9,11 +27,22 @@ export default class Ref<T extends { toString(): string } | string | null = stri
this.isString = typeof val === "string"; this.isString = typeof val === "string";
} }
watch(callback: (newVal: T) => void): void { watch(watcher: (newVal: T) => void): ISubscription {
if (this.watchers === null) { if (this.watchers === null) {
this.watchers = []; this.watchers = [];
} }
this.watchers.push(callback); this.watchers.push(watcher);
return new RefSubscription(() => this.unbind(watcher));
}
private unbind(watcher: (newVal: T) => void): void {
if (!this.watchers) {
return;
}
const index = this.watchers.indexOf(watcher);
if (index !== -1) {
this.watchers.splice(index, 1);
}
} }
get val(): T { get val(): T {

View File

@@ -35,11 +35,15 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
this.setBeat(options.beat); this.setBeat(options.beat);
} }
setBeat(beat: Beat): void { setBeat(beat: Beat | null): void {
this.beat = beat; if (beat) {
this.sub?.unbind(); this.beat = beat;
this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions); this.sub?.unbind();
this.redraw(); this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions);
this.redraw();
} else {
this.sub?.unbind();
}
} }
notify(publisher: unknown, event: EventTypeSubscriptions): void { notify(publisher: unknown, event: EventTypeSubscriptions): void {
@@ -58,7 +62,6 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
private rebuildBeatUnitViews() { private rebuildBeatUnitViews() {
const beatUnitCount = this.beat.getBarCount() * this.beat.getTimeSigUp(); const beatUnitCount = this.beat.getBarCount() * this.beat.getTimeSigUp();
this.beatUnitViews.splice(beatUnitCount, this.beatUnitViews.length - beatUnitCount);
for (let i = 0; i < beatUnitCount; i++) { for (let i = 0; i < beatUnitCount; i++) {
const beatUnit = this.beat.getUnitByIndex(i); const beatUnit = this.beat.getUnitByIndex(i);
if (beatUnit) { if (beatUnit) {
@@ -74,6 +77,8 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
} }
} }
} }
const deadViews = this.beatUnitViews.splice(beatUnitCount, this.beatUnitViews.length - beatUnitCount);
deadViews.forEach(beatUnitView => beatUnitView.setUnit(null));
} }
private onBeatUnitClick(button: number, index: number) { private onBeatUnitClick(button: number, index: number) {

View File

@@ -40,11 +40,18 @@ export default class BeatGroupView extends UINode implements ISubscriber<EventTy
} }
private setupBeatViews(): void { private setupBeatViews(): void {
this.beatViews = []; const newCount = this.beatGroup.getBeatCount();
for (let i = 0; i < this.beatGroup.getBeatCount(); i++) { for (let i = 0; i < newCount; i++) {
this.beatViews.unshift(new BeatView({beat: this.beatGroup.getBeatByIndex(i)})); const beat = this.beatGroup.getBeatByIndex(i);
if (beat && this.beatViews[i]) {
this.beatViews[i].setBeat(beat);
} else {
this.beatViews.push(new BeatView({beat: this.beatGroup.getBeatByIndex(i)}));
}
} }
if (this.currentOrientation === "vertical") { const deadBeatViews = this.beatViews.splice(newCount, this.beatViews.length - newCount);
deadBeatViews.forEach(beatView => beatView.setBeat(null));
if (this.currentOrientation === "horizontal") {
this.reverseDisplayOrder(); this.reverseDisplayOrder();
} }
} }
@@ -57,16 +64,16 @@ export default class BeatGroupView extends UINode implements ISubscriber<EventTy
} }
private reverseDisplayOrder(): void { private reverseDisplayOrder(): void {
this.beatViews = this.beatViews.reverse(); this.beatViews.reverse();
this.getNode().classList.toggle("vertical"); this.getNode().classList.toggle("vertical");
this.redraw(); this.redraw();
} }
setBeatGroup(newBeatGroup: BeatGroup): void { setBeatGroup(newBeatGroup: BeatGroup): void {
this.subscription.unbind();
this.beatGroup = newBeatGroup; this.beatGroup = newBeatGroup;
this.subscription.unbind();
this.subscription = this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged); this.subscription = this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged);
this.beatViews.forEach((beatView, i) => beatView.setBeat(this.beatGroup.getBeatByIndex(i))); this.setupBeatViews();
this.redraw(); this.redraw();
} }

View File

@@ -29,11 +29,28 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
this.setupBindings(); this.setupBindings();
} }
setUnit(beatUnit: BeatUnit): void { setUnit(beatUnit: BeatUnit | null): void {
this.beatUnit = beatUnit; if (beatUnit) {
this.setupBindings(); this.beatUnit = beatUnit;
this.notify(this.publisher, beatUnit.isOn() ? BeatUnitEvent.On : BeatUnitEvent.Off); this.setupBindings();
this.notify(this.publisher, BeatUnitEvent.TypeChange); this.notify(this.publisher, beatUnit.isOn() ? BeatUnitEvent.On : BeatUnitEvent.Off);
this.notify(this.publisher, BeatUnitEvent.TypeChange);
} else {
this.subscription?.unbind();
}
}
private setupBindings() {
this.subscription?.unbind();
this.subscription = this.beatUnit.addSubscriber(this, EventTypeSubscriptions);
this.mouseDownListeners.forEach(listener => this.getNode().removeEventListener("mousedown", listener));
this.hoverListeners.forEach(listener => this.getNode().removeEventListener("mouseover", listener));
this.redraw();
this.mouseDownListeners.forEach(listener => this.getNode().addEventListener("mousedown", listener));
this.hoverListeners.forEach(listener => this.getNode().addEventListener("mouseover", listener));
this.getNode().addEventListener("mousedown", (ev) => this.handleMouseDown(ev));
this.getNode().addEventListener("touchstart", (ev) => this.handleTouchStart(ev));
this.getNode().addEventListener("touchend", (ev) => this.handleTouchEnd(ev));
} }
private handleMouseDown(ev: MouseEvent): void { private handleMouseDown(ev: MouseEvent): void {
@@ -56,19 +73,6 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
} }
} }
private setupBindings() {
this.subscription?.unbind();
this.subscription = this.beatUnit.addSubscriber(this, EventTypeSubscriptions);
this.mouseDownListeners.forEach(listener => this.getNode().removeEventListener("mousedown", listener));
this.hoverListeners.forEach(listener => this.getNode().removeEventListener("mouseover", listener));
this.redraw();
this.mouseDownListeners.forEach(listener => this.getNode().addEventListener("mousedown", listener));
this.hoverListeners.forEach(listener => this.getNode().addEventListener("mouseover", listener));
this.getNode().addEventListener("mousedown", (ev) => this.handleMouseDown(ev));
this.getNode().addEventListener("touchstart", (ev) => this.handleTouchStart(ev));
this.getNode().addEventListener("touchend", (ev) => this.handleTouchEnd(ev));
}
toggle(): void { toggle(): void {
this.beatUnit.toggle(); this.beatUnit.toggle();
} }

View File

@@ -1,4 +1,5 @@
import Ref from "@/Ref"; import Ref from "@/Ref";
import {ISubscription} from "@/Publisher";
export type UINodeOptions = { export type UINodeOptions = {
@@ -52,38 +53,41 @@ export function h<
T extends keyof HTMLElementTagNameMap>( T extends keyof HTMLElementTagNameMap>(
type: T, type: T,
attributes: IRenderAttributes<T>, attributes: IRenderAttributes<T>,
subNodes?: (Node | UINode | Ref<any>)[], subNodes?: (Node | UINode | Ref)[],
): HTMLElementTagNameMap[T] { ): HTMLElementTagNameMap[T] {
const element = document.createElement(type); const element = document.createElement(type);
if (attributes) { if (attributes) {
for (const key in attributes) { for (const key in attributes) {
if (key === "classes") { if (!Object.prototype.hasOwnProperty.call(attributes, key)) {
element.classList.add(...attributes[key]!); continue;
} else if (key === "saveTo") { }
attributes.saveTo!.val = element; if (key === "classes" && attributes.classes) {
} else { element.classList.add(...attributes.classes);
} else if (key === "saveTo" && attributes.saveTo) {
attributes.saveTo.val = element;
} else if (Object.prototype.hasOwnProperty.call(attributes, key)) {
const attribute = (attributes as any)[key]; const attribute = (attributes as any)[key];
if (attribute instanceof Ref) { if (attribute) {
element[key as keyof HTMLElementTagNameMap[T]] = attribute.val; if (attribute instanceof Ref) {
attribute.watch((newVal) => element[key as keyof HTMLElementTagNameMap[T]] = newVal); if (element.hasAttribute(key)) {
} else { element.setAttribute(key, attribute.val);
element[key as keyof HTMLElementTagNameMap[T]] = attribute; attribute.watch((newVal) => {
if (element.hasAttribute(key)) {
element.setAttribute(key, newVal);
}
});
}
} else {
if (element.hasAttribute(key)) {
element.setAttribute(key, attribute);
}
}
} }
} }
} }
} }
if (subNodes) { if (subNodes) {
for (let i = 0; i < subNodes.length; i++) { attachSubs(element, subNodes);
const subNode = subNodes[i];
if (subNode instanceof UINode) {
element.append(subNode.render());
} else if (subNode instanceof Ref) {
subNode.watch((newVal) => element.childNodes.item(i).replaceWith(newVal.toString()));
element.append(q(subNode.val.toString()));
} else {
element.append(subNode);
}
}
} }
return element; return element;
} }
@@ -95,7 +99,31 @@ export function q(text: string): Text {
export function frag(subs?: Node[]): DocumentFragment { export function frag(subs?: Node[]): DocumentFragment {
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
if (subs) { if (subs) {
frag.append(...subs); attachSubs(frag, subs);
} }
return frag; return frag;
} }
function nodeRefWatcher<T>(newVal: T extends Ref<infer U> ? U : never, textNode: Text, sub: ISubscription): void {
if (!textNode.parentNode) {
sub.unbind();
textNode.remove();
} else {
textNode.replaceWith(newVal?.toString() ?? "null");
}
}
function attachSubs(node: Element | DocumentFragment, subNodes: (Node | UINode | Ref)[]): void {
for (let i = 0; i < subNodes.length; i++) {
const subNode = subNodes[i];
if (subNode instanceof UINode) {
node.append(subNode.render());
} else if (subNode instanceof Ref) {
const textNode = q(subNode.val.toString());
const sub = subNode.watch((newVal) => nodeRefWatcher<Ref>(newVal, textNode, sub));
node.append(textNode);
} else {
node.append(subNode);
}
}
}

View File

@@ -39,12 +39,12 @@ export default class BoolBoxView extends UINode {
build(): HTMLDivElement { build(): HTMLDivElement {
this.labelElement = h("label", { this.labelElement = h("label", {
classes: ["bool-box-label"], classes: ["bool-box-label"],
innerText: this.label,
onclick: () => { onclick: () => {
this.onInput(!this.checkboxElement.checked); this.onInput(!this.checkboxElement.checked);
}, },
}); });
if (this.label !== null) { if (this.label !== null) {
this.labelElement.innerText = this.label;
this.labelElement.classList.add("visible"); this.labelElement.classList.add("visible");
} }
this.checkboxElement = h("input", { this.checkboxElement = h("input", {