feat: performance improvements, bindings to refs can be unsubbed
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
35
src/Ref.ts
35
src/Ref.ts
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
if (beat) {
|
||||||
this.beat = beat;
|
this.beat = beat;
|
||||||
this.sub?.unbind();
|
this.sub?.unbind();
|
||||||
this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions);
|
this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions);
|
||||||
this.redraw();
|
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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
if (beatUnit) {
|
||||||
this.beatUnit = beatUnit;
|
this.beatUnit = beatUnit;
|
||||||
this.setupBindings();
|
this.setupBindings();
|
||||||
this.notify(this.publisher, beatUnit.isOn() ? BeatUnitEvent.On : BeatUnitEvent.Off);
|
this.notify(this.publisher, beatUnit.isOn() ? BeatUnitEvent.On : BeatUnitEvent.Off);
|
||||||
this.notify(this.publisher, BeatUnitEvent.TypeChange);
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
if (attribute instanceof Ref) {
|
if (attribute instanceof Ref) {
|
||||||
element[key as keyof HTMLElementTagNameMap[T]] = attribute.val;
|
if (element.hasAttribute(key)) {
|
||||||
attribute.watch((newVal) => element[key as keyof HTMLElementTagNameMap[T]] = newVal);
|
element.setAttribute(key, attribute.val);
|
||||||
|
attribute.watch((newVal) => {
|
||||||
|
if (element.hasAttribute(key)) {
|
||||||
|
element.setAttribute(key, newVal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
element[key as keyof HTMLElementTagNameMap[T]] = attribute;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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", {
|
||||||
|
|||||||
Reference in New Issue
Block a user