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";
class Subscription<EventType extends LEvent, PublisherType> implements ISubscription {
class PublisherSubscription<EventType extends LEvent> implements ISubscription {
private subscriber: ISubscriber<EventType>;
private readonly eventTypes: EventType[];
private publisher: Publisher<EventType, PublisherType>;
constructor(publisher: Publisher<EventType, PublisherType>, subscriber: ISubscriber<EventType>, eventTypes: EventType[]) {
private unbindCallback?: () => void;
constructor(subscriber: ISubscriber<EventType>, eventTypes: EventType[]) {
this.subscriber = subscriber;
this.publisher = publisher;
this.eventTypes = eventTypes;
}
setUnbind(unbind: () => void): void {
this.unbindCallback = unbind;
}
unbind(): void {
this.publisher.unbind(this);
this.unbindCallback?.();
}
getEventTypes(): EventType[] {
@@ -48,10 +52,12 @@ export class Publisher<EventType extends LEvent, PublisherType> implements IPubl
for (const key of eventTypes) {
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()) {
const subs = this.getSubscribers(key);
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 value: T;
private asString?: string;
@@ -9,11 +27,22 @@ export default class Ref<T extends { toString(): string } | string | null = stri
this.isString = typeof val === "string";
}
watch(callback: (newVal: T) => void): void {
watch(watcher: (newVal: T) => void): ISubscription {
if (this.watchers === null) {
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 {

View File

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

View File

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

View File

@@ -29,11 +29,28 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
this.setupBindings();
}
setUnit(beatUnit: BeatUnit): void {
this.beatUnit = beatUnit;
this.setupBindings();
this.notify(this.publisher, beatUnit.isOn() ? BeatUnitEvent.On : BeatUnitEvent.Off);
this.notify(this.publisher, BeatUnitEvent.TypeChange);
setUnit(beatUnit: BeatUnit | null): void {
if (beatUnit) {
this.beatUnit = beatUnit;
this.setupBindings();
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 {
@@ -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 {
this.beatUnit.toggle();
}

View File

@@ -1,4 +1,5 @@
import Ref from "@/Ref";
import {ISubscription} from "@/Publisher";
export type UINodeOptions = {
@@ -52,38 +53,41 @@ export function h<
T extends keyof HTMLElementTagNameMap>(
type: T,
attributes: IRenderAttributes<T>,
subNodes?: (Node | UINode | Ref<any>)[],
subNodes?: (Node | UINode | Ref)[],
): HTMLElementTagNameMap[T] {
const element = document.createElement(type);
if (attributes) {
for (const key in attributes) {
if (key === "classes") {
element.classList.add(...attributes[key]!);
} else if (key === "saveTo") {
attributes.saveTo!.val = element;
} else {
if (!Object.prototype.hasOwnProperty.call(attributes, key)) {
continue;
}
if (key === "classes" && attributes.classes) {
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];
if (attribute instanceof Ref) {
element[key as keyof HTMLElementTagNameMap[T]] = attribute.val;
attribute.watch((newVal) => element[key as keyof HTMLElementTagNameMap[T]] = newVal);
} else {
element[key as keyof HTMLElementTagNameMap[T]] = attribute;
if (attribute) {
if (attribute instanceof Ref) {
if (element.hasAttribute(key)) {
element.setAttribute(key, attribute.val);
attribute.watch((newVal) => {
if (element.hasAttribute(key)) {
element.setAttribute(key, newVal);
}
});
}
} else {
if (element.hasAttribute(key)) {
element.setAttribute(key, attribute);
}
}
}
}
}
}
if (subNodes) {
for (let i = 0; i < subNodes.length; i++) {
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);
}
}
attachSubs(element, subNodes);
}
return element;
}
@@ -95,7 +99,31 @@ export function q(text: string): Text {
export function frag(subs?: Node[]): DocumentFragment {
const frag = document.createDocumentFragment();
if (subs) {
frag.append(...subs);
attachSubs(frag, subs);
}
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 {
this.labelElement = h("label", {
classes: ["bool-box-label"],
innerText: this.label,
onclick: () => {
this.onInput(!this.checkboxElement.checked);
},
});
if (this.label !== null) {
this.labelElement.innerText = this.label;
this.labelElement.classList.add("visible");
}
this.checkboxElement = h("input", {