From aefcbd28dbe066e543c6a790630875e50ccf0302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilyas=20T=C3=BCrkben?= Date: Mon, 16 Dec 2024 10:40:07 +0100 Subject: [PATCH] refactor: lazy init, support for delayed observers --- README.md | 2 +- dist/picosm.js | 152 +++++++++++++++++------------------- src/LitObserver.js | 15 +--- src/makeObservable.js | 69 ++++++++-------- src/reaction.js | 37 +++------ src/track.js | 19 +---- test/LitObserver.test.js | 5 +- test/TestStore.js | 7 +- test/makeObservable.test.js | 4 +- 9 files changed, 133 insertions(+), 177 deletions(-) diff --git a/README.md b/README.md index ae54031..f886ea5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Currently, the non minified bundle size is `4938 bytes` and is around `~1.2kb` g - `reaction`: react to specific changes - `computed`: cache computed values -Also, an opinionated approach for tracking nested dependencies with `track/untrack` functions is added.
+Also, an opinionated approach for tracking nested dependencies with `track` functions is added.
If an observable wants to get notified by another one, these functions can be used. ## How to Use diff --git a/dist/picosm.js b/dist/picosm.js index d445f6c..4827c5e 100644 --- a/dist/picosm.js +++ b/dist/picosm.js @@ -8,14 +8,14 @@ function instrumentAction(target, methodName) { descriptor.value = async function(...args) { const response = await originalMethod.call(this, ...args); this.__resetComputedProperties(); - this.__notifyListeners(); + this.__notifyObservers(); return response; }; } else { descriptor.value = function(...args) { const response = originalMethod.call(this, ...args); this.__resetComputedProperties(); - this.__notifyListeners(); + this.__notifyObservers(); return response; }; } @@ -28,11 +28,22 @@ function instrumentComputed(target, getterName) { if (descriptor && typeof descriptor.get === "function") { const originalGetter = descriptor.get; descriptor.get = function() { - if (this.__computedValues.has(getterName)) { - return this.__computedValues.get(getterName); + if (!this.__computedProperties) { + Object.defineProperties( + this, + { + __computedProperties: { value: /* @__PURE__ */ new Map() } + }, + { + __computedProperties: { enumerable: false, writable: false } + } + ); + } + if (this.__computedProperties.has(getterName)) { + return this.__computedProperties.get(getterName); } const cachedValue = originalGetter.call(this); - this.__computedValues.set(getterName, cachedValue); + this.__computedProperties.set(getterName, cachedValue); return cachedValue; }; Object.defineProperty(prototype, getterName, descriptor); @@ -40,31 +51,26 @@ function instrumentComputed(target, getterName) { } function makeObservable(constructor2, actions = [], computeds = []) { class SuperClass extends constructor2 { - constructor(...args) { - super(...args); - Object.defineProperties( - this, - { - __observers: { value: /* @__PURE__ */ new Set() }, - __computedValues: { value: /* @__PURE__ */ new Map() }, - __dependencies: { value: /* @__PURE__ */ new WeakMap() } - }, - { - __observers: { enumerable: false, writable: false }, - __computedValues: { enumerable: false, writable: false }, - __dependencies: { enumerable: false, writable: false } - } - ); - } - __notifyListeners() { - this.__observers.forEach((listener) => { + __notifyObservers() { + this.__observers?.forEach((listener) => { listener(); }); } __resetComputedProperties() { - this.__computedValues.clear(); + this.__computedProperties?.clear(); } __observe(callback) { + if (!this.__observers) { + Object.defineProperties( + this, + { + __observers: { value: /* @__PURE__ */ new Set() } + }, + { + __observers: { enumerable: false, writable: false } + } + ); + } this.__observers.add(callback); return () => { this.__observers.delete(callback); @@ -79,63 +85,55 @@ function makeObservable(constructor2, actions = [], computeds = []) { }); return SuperClass; } -function observe(target, callback) { - return target.__observe(callback); -} -function observeSlow(timeout) { - return (target, callback) => { - let timer; - const listener = () => { - clearTimeout(timer); - timer = setTimeout(callback, timeout); - }; - return target.__observe(listener); +function observeSlow(target, callback, timeout) { + let timer; + const listener = () => { + clearTimeout(timer); + timer = setTimeout(callback, timeout); }; + return target.__observe(listener); +} +function observe(target, callback, timeout) { + if (timeout) { + return observeSlow(target, callback, timeout); + } else { + return target.__observe(callback); + } } // src/reaction.js -function reaction(target, callback, execute) { +function reaction(target, callback, execute, timeout) { let lastProps = []; - return target.__observe(async () => { - const props = callback(target); - if (lastProps === props) - return; - let shouldExecute = false; - for (let i = 0; i < props.length; i++) { - if (lastProps[i] !== props[i]) { - shouldExecute = true; - break; + return observe( + target, + async () => { + const props = callback(target); + if (lastProps === props) + return; + let shouldExecute = false; + for (let i = 0; i < props.length; i++) { + if (lastProps[i] !== props[i]) { + shouldExecute = true; + break; + } } - } - if (shouldExecute) { - lastProps = props; - execute(...props); - } - }); + if (shouldExecute) { + lastProps = props; + execute(...props); + } + }, + timeout + ); } // src/track.js function track(target, source) { - if (!target.__observers || !source?.__observers) - return; - const disposer = source.__observe(() => { + const disposer = source.__observe?.(() => { target.__resetComputedProperties(); - target.__notifyListeners(); + target.__notifyObservers(); }); - target.__dependencies.set(source, disposer); target.__resetComputedProperties(); - target.__notifyListeners(); -} -function untrack(target, source) { - if (!target.__observers || !source?.__observers) - return; - const disposer = target.__dependencies.get(source); - if (disposer) { - target.__dependencies.delete(source); - disposer(); - target.__resetComputedProperties(); - target.__notifyListeners(); - } + target.__notifyObservers(); } // src/LitObserver.js @@ -150,22 +148,20 @@ function litObserver(constructor, properties) { properties.forEach((property) => { let observableProperty; let delay; - let observeFn2 = observe; if (Array.isArray(property)) { observableProperty = this[property[0]]; delay = property[1]; - observeFn2 = observeSlow(delay); } else { observableProperty = this[property]; } - if (!observableProperty?.__observers) - return; if (this.#observables.has(observableProperty)) { return; } + if (!observableProperty) + return; this.#observables.add(observableProperty); this.#disposers.add( - observeFn2(observableProperty, this.requestUpdate.bind(this)) + observe(observableProperty, this.requestUpdate.bind(this), delay) ); }); } @@ -173,12 +169,6 @@ function litObserver(constructor, properties) { super.updated(changedProperties); this.trackProperties(); } - connectedCallback() { - super.connectedCallback(); - this.#observables.forEach((o) => { - this.#disposers.add(observeFn(o, this.requestUpdate.bind(this))); - }); - } disconnectedCallback() { super.disconnectedCallback(); this.#disposers.forEach((disposer) => { @@ -193,8 +183,6 @@ export { litObserver, makeObservable, observe, - observeSlow, reaction, - track, - untrack + track }; diff --git a/src/LitObserver.js b/src/LitObserver.js index 499a383..2a54992 100644 --- a/src/LitObserver.js +++ b/src/LitObserver.js @@ -1,4 +1,4 @@ -import { observe, observeSlow } from './makeObservable.js'; +import { observe } from './makeObservable.js'; export function litObserver(constructor, properties) { class LitObserver extends constructor { @@ -12,21 +12,19 @@ export function litObserver(constructor, properties) { properties.forEach((property) => { let observableProperty; let delay; - let observeFn = observe; if (Array.isArray(property)) { observableProperty = this[property[0]]; delay = property[1]; - observeFn = observeSlow(delay); } else { observableProperty = this[property]; } - if (!observableProperty?.__observers) return; if (this.#observables.has(observableProperty)) { return; } + if (!observableProperty) return; this.#observables.add(observableProperty); this.#disposers.add( - observeFn(observableProperty, this.requestUpdate.bind(this)), + observe(observableProperty, this.requestUpdate.bind(this), delay), ); }); } @@ -36,13 +34,6 @@ export function litObserver(constructor, properties) { this.trackProperties(); } - connectedCallback() { - super.connectedCallback(); - this.#observables.forEach((o) => { - this.#disposers.add(observeFn(o, this.requestUpdate.bind(this))); - }); - } - disconnectedCallback() { super.disconnectedCallback(); this.#disposers.forEach((disposer) => { diff --git a/src/makeObservable.js b/src/makeObservable.js index 4b8e75c..cf698d0 100644 --- a/src/makeObservable.js +++ b/src/makeObservable.js @@ -8,14 +8,14 @@ function instrumentAction(target, methodName) { descriptor.value = async function (...args) { const response = await originalMethod.call(this, ...args); this.__resetComputedProperties(); - this.__notifyListeners(); + this.__notifyObservers(); return response; }; } else { descriptor.value = function (...args) { const response = originalMethod.call(this, ...args); this.__resetComputedProperties(); - this.__notifyListeners(); + this.__notifyObservers(); return response; }; } @@ -32,6 +32,17 @@ function instrumentComputed(target, getterName) { const originalGetter = descriptor.get; descriptor.get = function () { + if (!this.__computedProperties) { + Object.defineProperties( + this, + { + __computedProperties: { value: new Map() }, + }, + { + __computedProperties: { enumerable: false, writable: false }, + }, + ); + } if (this.__computedProperties.has(getterName)) { return this.__computedProperties.get(getterName); } @@ -46,24 +57,7 @@ function instrumentComputed(target, getterName) { export function makeObservable(constructor, actions = [], computeds = []) { class SuperClass extends constructor { - constructor(...args) { - super(...args); - Object.defineProperties( - this, - { - __observers: { value: new Set() }, - __computedProperties: { value: new Map() }, - __dependencies: { value: new WeakMap() }, - }, - { - __observers: { enumerable: false, writable: false }, - __computedProperties: { enumerable: false, writable: false }, - __dependencies: { enumerable: false, writable: false }, - }, - ); - } - - __notifyListeners() { + __notifyObservers() { this.__observers?.forEach((listener) => { listener(); }); @@ -74,6 +68,17 @@ export function makeObservable(constructor, actions = [], computeds = []) { } __observe(callback) { + if (!this.__observers) { + Object.defineProperties( + this, + { + __observers: { value: new Set() }, + }, + { + __observers: { enumerable: false, writable: false }, + }, + ); + } this.__observers.add(callback); return () => { this.__observers.delete(callback); @@ -90,17 +95,19 @@ export function makeObservable(constructor, actions = [], computeds = []) { return SuperClass; } -export function observe(target, callback) { - return target.__observe(callback); +function observeSlow(target, callback, timeout) { + let timer; + const listener = () => { + clearTimeout(timer); + timer = setTimeout(callback, timeout); + }; + return target.__observe(listener); } -export function observeSlow(timeout) { - return (target, callback) => { - let timer; - const listener = () => { - clearTimeout(timer); - timer = setTimeout(callback, timeout); - }; - return target.__observe(listener); - }; +export function observe(target, callback, timeout) { + if (timeout) { + return observeSlow(target, callback, timeout); + } else { + return target.__observe(callback); + } } diff --git a/src/reaction.js b/src/reaction.js index 24589ef..41b132f 100644 --- a/src/reaction.js +++ b/src/reaction.js @@ -1,9 +1,10 @@ -import { observe, observeSlow } from './makeObservable.js'; +import { observe } from './makeObservable.js'; -export function reaction(target, callback, execute) { - setTimeout(() => { - let lastProps = []; - return observe(target, async () => { +export function reaction(target, callback, execute, timeout) { + let lastProps = []; + return observe( + target, + async () => { const props = callback(target); if (lastProps === props) return; let shouldExecute = false; @@ -17,27 +18,7 @@ export function reaction(target, callback, execute) { lastProps = props; execute(...props); } - }); - }, 0); -} - -export function reactionSlow(target, callback, execute, timeout) { - setTimeout(() => { - let lastProps = []; - return observeSlow(timeout)(target, async () => { - const props = callback(target); - if (lastProps === props) return; - let shouldExecute = false; - for (let i = 0; i < props.length; i++) { - if (lastProps[i] !== props[i]) { - shouldExecute = true; - break; - } - } - if (shouldExecute) { - lastProps = props; - execute(...props); - } - }); - }, 0); + }, + timeout, + ); } diff --git a/src/track.js b/src/track.js index f59626e..9fb1a9a 100644 --- a/src/track.js +++ b/src/track.js @@ -1,21 +1,8 @@ export function track(target, source) { - if (!target.__observers || !source?.__observers) return; - const disposer = source.__observe(() => { + const disposer = source.__observe?.(() => { target.__resetComputedProperties(); - target.__notifyListeners(); + target.__notifyObservers(); }); - target.__dependencies.set(source, disposer); target.__resetComputedProperties(); - target.__notifyListeners(); -} - -export function untrack(target, source) { - if (!target.__observers || !source?.__observers) return; - const disposer = target.__dependencies.get(source); - if (disposer) { - target.__dependencies.delete(source); - disposer(); - target.__resetComputedProperties(); - target.__notifyListeners(); - } + target.__notifyObservers(); } diff --git a/test/LitObserver.test.js b/test/LitObserver.test.js index 7cd3e62..1215911 100644 --- a/test/LitObserver.test.js +++ b/test/LitObserver.test.js @@ -6,6 +6,7 @@ import { litObserver } from '../src/LitObserver.js'; class User { firstName = 'John'; lastName = ''; + setLastName(name) { this.lastName = name; } @@ -37,7 +38,7 @@ class HelloWorldSlow extends LitElement { } customElements.define( 'hello-world-slow', - litObserver(HelloWorld, [['user', 1000]]), + litObserver(HelloWorld, [['user', 500]]), ); describe('LitOserver', () => { @@ -65,7 +66,7 @@ describe('LitOserver', () => { helloWorldSlow.user.setLastName('Doe'); await helloWorldSlow.updateComplete; expect(helloWorldSlow.shadowRoot.textContent).to.equal('Hello, John !'); - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 600)); expect(helloWorldSlow.shadowRoot.textContent).to.equal('Hello, John Doe!'); }); }); diff --git a/test/TestStore.js b/test/TestStore.js index b348788..cd1860d 100644 --- a/test/TestStore.js +++ b/test/TestStore.js @@ -1,6 +1,7 @@ -import { track, untrack } from '../src/track.js'; +import { track } from '../src/track.js'; export default class TestStore { + #disposer; constructor() { this.checked = false; this._counter = 0; @@ -28,11 +29,11 @@ export default class TestStore { set test(test) { if (this._test) { - untrack(this, this._test); + this.#disposer?.(); } this._test = test; if (test) { - track(this, test); + this.#disposer = track(this, test); } } diff --git a/test/makeObservable.test.js b/test/makeObservable.test.js index dc63936..bbea0f0 100644 --- a/test/makeObservable.test.js +++ b/test/makeObservable.test.js @@ -11,12 +11,12 @@ describe('Pico State Manager', () => { ); it('makes any class observable', () => { - expect(TestObservable.prototype.__notifyListeners).to.be.a('function'); + expect(TestObservable.prototype.__notifyObservers).to.be.a('function'); expect(TestObservable.prototype.__resetComputedProperties).to.be.a( 'function', ); expect(TestObservable.prototype.__observe).to.be.a('function'); - expect(TestObservable.prototype.__notifyListeners).to.be.a('function'); + expect(TestObservable.prototype.__notifyObservers).to.be.a('function'); }); it('caches computed values', () => {