From 0f1fd945713da313daa955dad1f6979dc1ef8e79 Mon Sep 17 00:00:00 2001 From: Esther Brunner Date: Fri, 30 Aug 2024 14:11:16 +0200 Subject: [PATCH] slightly optimized scheduler --- README.md | 2 +- cause-effect.js | 42 +++++++++++++++++------------------ cause-effect.min.js | 2 +- index.js | 42 +++++++++++++++++------------------ index.min.js | 2 +- index.ts | 2 +- package.json | 2 +- src/cause-effect.ts | 2 +- src/core/scheduler.ts | 46 +++++++++++++++++++++++---------------- src/ui-element.ts | 2 +- types/core/scheduler.d.ts | 7 +++--- types/ui-element.d.ts | 2 +- 12 files changed, 81 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 2424ca1..d65a2f4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ UIElement - the "look ma, no JS framework!" library bringing signals-based reactivity to vanilla Web Components -Version 0.8.1 +Version 0.8.2 ## What is UIElement? diff --git a/cause-effect.js b/cause-effect.js index de9e350..943a81e 100644 --- a/cause-effect.js +++ b/cause-effect.js @@ -33,24 +33,6 @@ const scheduler = () => { const effectQueue = new Map(); const cleanupQueue = new Map(); let requestId; - const requestTick = () => { - if (requestId) - cancelAnimationFrame(requestId); - requestId = requestAnimationFrame(flush); - }; - const enqueue = (element, prop, fn) => { - if (!effectQueue.has(element)) - effectQueue.set(element, new Map()); - const elEffects = effectQueue.get(element); - if (!elEffects.has(prop)) - elEffects.set(prop, fn); - requestTick(); - }; - const cleanup = (effect, fn) => { - if (!cleanupQueue.has(effect)) - cleanupQueue.set(effect, fn); - requestTick(); - }; const run = (fn, msg) => { try { fn(); @@ -70,8 +52,27 @@ const scheduler = () => { run(fn, 'Cleanup failed'); cleanupQueue.clear(); }; + const requestTick = () => { + if (requestId) + cancelAnimationFrame(requestId); + requestId = requestAnimationFrame(flush); + }; + const getEffectMap = (key) => { + if (!effectQueue.has(key)) + effectQueue.set(key, new Map()); + return effectQueue.get(key); + }; + const addToQueue = (map) => (key, fn) => { + const more = !map.has(key); + map.set(key, fn); + if (more) + requestTick(); + }; queueMicrotask(flush); // initial flush when the call stack is empty - return { enqueue, cleanup }; + return { + enqueue: (element, prop, fn) => addToQueue(getEffectMap(element))(prop, fn), + cleanup: addToQueue(cleanupQueue) + }; }; /* === Internal === */ @@ -174,8 +175,7 @@ const effect = (fn) => { activeEffect = n; const cleanupFn = fn((element, prop, callback) => { enqueue(element, prop, callback); - if (!targets.has(element)) - targets.add(element); + targets.add(element); }); if (isFunction(cleanupFn)) cleanup(n, cleanupFn); diff --git a/cause-effect.min.js b/cause-effect.min.js index 2f50338..0b1d0c3 100644 --- a/cause-effect.min.js +++ b/cause-effect.min.js @@ -1 +1 @@ -const e=e=>t=>typeof t===e,t=e("object"),n=e("function"),c=e=>(e=>null!=e)(e)&&(t(e)||n(e)),s=(e,t)=>n(e[t]),o="error",r=(e,t,n="debug")=>((e=>"warn"===e||e===o)(n)&&console[n](t,e),e);let a;const{enqueue:f,cleanup:u}=(()=>{const e=new Map,t=new Map;let n;const c=()=>{n&&cancelAnimationFrame(n),n=requestAnimationFrame(a)},s=(e,t)=>{try{e()}catch(e){r(e,t,o)}},a=()=>{n=null;for(const[t,n]of e)for(const[e,c]of n)s(c(t),` Effect ${e} on ${t?.localName||"unknown"} failed`);e.clear();for(const e of t.values())s(e,"Cleanup failed");t.clear()};return queueMicrotask(a),{enqueue:(t,n,s)=>{e.has(t)||e.set(t,new Map);const o=e.get(t);o.has(n)||o.set(n,s),c()},cleanup:(e,n)=>{t.has(e)||t.set(e,n),c()}}})(),l=e=>{for(const t of e)t.run()},i=e=>c(e)&&s(e,"set"),d=e=>i(e)||(e=>c(e)&&s(e,"run")&&"effects"in e)(e),p=e=>{const t=()=>(a&&t.effects.add(a),e);return t.effects=new Set,t.set=c=>{const s=e;e=n(c)&&!d(c)?c(e):c,Object.is(e,s)||l(t.effects)},t},w=(e,t=!1)=>{let n,c=!0;const s=()=>{if(a&&s.effects.add(a),t&&!c)return n;const o=a;return a=s,n=e(),c=!1,a=o,n};return s.effects=new Set,s.run=()=>{c=!0,t&&l(s.effects)},s},h=e=>{const t=new Set,c=()=>{const s=a;a=c;const o=e(((e,n,c)=>{f(e,n,c),t.has(e)||t.add(e)}));n(o)&&u(c,o),a=s};c.run=()=>c(),c.targets=t,c()};export{p as cause,w as derive,h as effect,d as isSignal,i as isState}; +const e=e=>t=>typeof t===e,t=e("object"),n=e("function"),c=e=>(e=>null!=e)(e)&&(t(e)||n(e)),o=(e,t)=>n(e[t]),s="error",r=(e,t,n="debug")=>((e=>"warn"===e||e===s)(n)&&console[n](t,e),e);let f;const{enqueue:a,cleanup:u}=(()=>{const e=new Map,t=new Map;let n;const c=(e,t)=>{try{e()}catch(e){r(e,t,s)}},o=()=>{n=null;for(const[t,n]of e)for(const[e,o]of n)c(o(t),` Effect ${e} on ${t?.localName||"unknown"} failed`);e.clear();for(const e of t.values())c(e,"Cleanup failed");t.clear()},f=e=>(t,c)=>{const s=!e.has(t);e.set(t,c),s&&(n&&cancelAnimationFrame(n),n=requestAnimationFrame(o))};return queueMicrotask(o),{enqueue:(t,n,c)=>{return f((o=t,e.has(o)||e.set(o,new Map),e.get(o)))(n,c);var o},cleanup:f(t)}})(),l=e=>{for(const t of e)t.run()},i=e=>c(e)&&o(e,"set"),d=e=>i(e)||(e=>c(e)&&o(e,"run")&&"effects"in e)(e),p=e=>{const t=()=>(f&&t.effects.add(f),e);return t.effects=new Set,t.set=c=>{const o=e;e=n(c)&&!d(c)?c(e):c,Object.is(e,o)||l(t.effects)},t},w=(e,t=!1)=>{let n,c=!0;const o=()=>{if(f&&o.effects.add(f),t&&!c)return n;const s=f;return f=o,n=e(),c=!1,f=s,n};return o.effects=new Set,o.run=()=>{c=!0,t&&l(o.effects)},o},m=e=>{const t=new Set,c=()=>{const o=f;f=c;const s=e(((e,n,c)=>{a(e,n,c),t.add(e)}));n(s)&&u(c,s),f=o};c.run=()=>c(),c.targets=t,c()};export{p as cause,w as derive,m as effect,d as isSignal,i as isState}; diff --git a/index.js b/index.js index dd080c2..30a1f5f 100644 --- a/index.js +++ b/index.js @@ -94,24 +94,6 @@ const scheduler = () => { const effectQueue = new Map(); const cleanupQueue = new Map(); let requestId; - const requestTick = () => { - if (requestId) - cancelAnimationFrame(requestId); - requestId = requestAnimationFrame(flush); - }; - const enqueue = (element, prop, fn) => { - if (!effectQueue.has(element)) - effectQueue.set(element, new Map()); - const elEffects = effectQueue.get(element); - if (!elEffects.has(prop)) - elEffects.set(prop, fn); - requestTick(); - }; - const cleanup = (effect, fn) => { - if (!cleanupQueue.has(effect)) - cleanupQueue.set(effect, fn); - requestTick(); - }; const run = (fn, msg) => { try { fn(); @@ -131,8 +113,27 @@ const scheduler = () => { run(fn, 'Cleanup failed'); cleanupQueue.clear(); }; + const requestTick = () => { + if (requestId) + cancelAnimationFrame(requestId); + requestId = requestAnimationFrame(flush); + }; + const getEffectMap = (key) => { + if (!effectQueue.has(key)) + effectQueue.set(key, new Map()); + return effectQueue.get(key); + }; + const addToQueue = (map) => (key, fn) => { + const more = !map.has(key); + map.set(key, fn); + if (more) + requestTick(); + }; queueMicrotask(flush); // initial flush when the call stack is empty - return { enqueue, cleanup }; + return { + enqueue: (element, prop, fn) => addToQueue(getEffectMap(element))(prop, fn), + cleanup: addToQueue(cleanupQueue) + }; }; /* === Internal === */ @@ -204,8 +205,7 @@ const effect = (fn) => { activeEffect = n; const cleanupFn = fn((element, prop, callback) => { enqueue(element, prop, callback); - if (!targets.has(element)) - targets.add(element); + targets.add(element); }); if (isFunction(cleanupFn)) cleanup(n, cleanupFn); diff --git a/index.min.js b/index.min.js index 509b82c..599a26e 100644 --- a/index.min.js +++ b/index.min.js @@ -1 +1 @@ -const t=t=>e=>typeof e===t,e=t("object"),s=t("function"),r=t=>null==t,n=t=>null!=t,o=t=>n(t)&&(e(t)||s(t)),a=(t,e)=>s(t[e]),c=t=>t.nodeType!==Node.COMMENT_NODE,i=t=>r(t)?[]:[t],l=t=>t.shadowRoot||t,u=(t,e)=>({host:t,target:e}),h="error",g=(t,e,s="debug")=>((t=>"warn"===t||t===h)(s)&&console[s](e,t),t);let d;const{enqueue:f,cleanup:p}=(()=>{const t=new Map,e=new Map;let s;const r=()=>{s&&cancelAnimationFrame(s),s=requestAnimationFrame(o)},n=(t,e)=>{try{t()}catch(t){g(t,e,h)}},o=()=>{s=null;for(const[e,s]of t)for(const[t,r]of s)n(r(e),` Effect ${t} on ${e?.localName||"unknown"} failed`);t.clear();for(const t of e.values())n(t,"Cleanup failed");e.clear()};return queueMicrotask(o),{enqueue:(e,s,n)=>{t.has(e)||t.set(e,new Map);const o=t.get(e);o.has(s)||o.set(s,n),r()},cleanup:(t,s)=>{e.has(t)||e.set(t,s),r()}}})(),b=t=>o(t)&&a(t,"set"),m=t=>b(t)||(t=>o(t)&&a(t,"run")&&"effects"in t)(t),y=t=>{const e=()=>(d&&e.effects.add(d),t);return e.effects=new Set,e.set=r=>{const n=t;t=s(r)&&!m(r)?r(t):r,Object.is(t,n)||(t=>{for(const e of t)e.run()})(e.effects)},e},v=t=>{const e=new Set,r=()=>{const n=d;d=r;const o=t(((t,s,r)=>{f(t,s,r),e.has(t)||e.add(t)}));s(o)&&p(r,o),d=n};r.run=()=>r(),r.targets=e,r()},w="context-request";class x extends Event{context;callback;subscribe;constructor(t,e,s=!1){super(w,{bubbles:!0,composed:!0}),this.context=t,this.callback=e,this.subscribe=s}}const E=t=>async({host:e,target:r})=>{await e.constructor.registry.whenDefined(r.localName);for(const[n,o]of Object.entries(t))r.set(n,m(o)?o:s(o)?y(o):e.signal(o));return{host:e,target:r}},C=(t,e)=>({host:s,target:r})=>(r.addEventListener(t,e),{host:s,target:r}),N=(t,e)=>({host:s,target:r})=>(r.removeEventListener(t,e),{host:s,target:r}),S=(t,e=t)=>({host:s,target:r})=>(v((()=>{r.dispatchEvent(new CustomEvent(t,{detail:s.get(e),bubbles:!0}))})),{host:s,target:r}),A=t=>[n(t[0])],M=t=>t.map((t=>parseInt(t,10))).filter(Number.isFinite),k=t=>t.map(parseFloat).filter(Number.isFinite),$=t=>t,q=t=>{let e=[];try{e=t.map((t=>JSON.parse(t)))}catch(t){g(t,"Failed to parse JSON",h)}return e},L=(t,e,s,n,o,a,c)=>{const i=a;return t.set(e,o,!1),v((o=>{if(t.has(e)){const a=t.get(e);r(a)?o(s,n,i):o(s,n,(t=>c(t))(a))}})),{host:t,target:s}},F=t=>({host:e,target:s})=>{const r=s.textContent||"",n=t=>e=>()=>{Array.from(e.childNodes).filter(c).forEach((t=>t.remove())),e.append(document.createTextNode(t))};return L(e,t,s,`t-${String(t)}`,r,n(r),n)},O=(t,e=t)=>({host:s,target:r})=>{const n=e=>s=>()=>s[t]=e;return L(s,e,r,`p-${String(t)}`,r[t],n(null),n)},T=(t,e=t)=>({host:s,target:r})=>L(s,e,r,`a-${t}`,r.getAttribute(t),(e=>()=>e.removeAttribute(t)),(e=>s=>()=>s.setAttribute(t,e))),j=(t,e=t)=>({host:s,target:r})=>{const n=e=>s=>()=>s.toggleAttribute(t,e);return L(s,e,r,`a-${t}`,r.hasAttribute(t),n(!1),n)},D=(t,e=t)=>({host:s,target:r})=>L(s,e,r,`c-${t}`,r.classList.contains(t),(e=>()=>e.classList.remove(t)),(e=>s=>()=>s.classList.toggle(t,e))),J=(t,e=t)=>({host:s,target:r})=>L(s,e,r,`s-${t}`,r.style[t],(e=>()=>e.style.removeProperty(t)),(e=>s=>()=>s.style[t]=e)),P=t=>s(t)?P(t()):t;class H extends HTMLElement{static registry=customElements;static attributeMap={};static consumedContexts;static providedContexts;static define(t){try{this.registry.get(t)||this.registry.define(t,this)}catch(e){g(t,e.message,h)}}#t=new Map;self=[u(this,this)];attributeChangedCallback(t,e,r){if(r===e)return;const n=this.constructor.attributeMap[t];this.set(t,s(n)?n(i(r),this,e)[0]:r)}connectedCallback(){const t=this.constructor,e=t.consumedContexts||[];for(const t of e)this.set(String(t),void 0);setTimeout((()=>{for(const t of e)this.dispatchEvent(new x(t,(e=>this.set(String(t),e))))}));const r=t.providedContexts||[];r.length&&this.addEventListener(w,(t=>{const{context:e,callback:n}=t;r.includes(e)&&s(n)&&(t.stopPropagation(),n(this.#t.get(String(e))))}))}disconnectedCallback(){}has(t){return this.#t.has(t)}get(t){return P(this.#t.get(t))}set(t,e,s=!0){if(this.#t.has(t)){if(s){const s=this.#t.get(t);b(s)&&s.set(e)}}else this.#t.set(t,m(e)?e:y(e))}delete(t){return this.#t.delete(t)}signal(t){return this.#t.get(t)}first=(t=>e=>i(l(t).querySelector(e)).map((e=>u(t,e))))(this);all=(t=>e=>Array.from(l(t).querySelectorAll(e)).map((e=>u(t,e))))(this)}export{H as UIElement,A as asBoolean,M as asInteger,q as asJSON,k as asNumber,$ as asString,S as dispatch,v as effect,g as log,i as maybe,N as off,C as on,E as pass,T as setAttribute,O as setProperty,J as setStyle,F as setText,j as toggleAttribute,D as toggleClass}; +const t=t=>e=>typeof e===t,e=t("object"),s=t("function"),r=t=>null==t,n=t=>null!=t,o=t=>n(t)&&(e(t)||s(t)),a=(t,e)=>s(t[e]),c=t=>t.nodeType!==Node.COMMENT_NODE,i=t=>r(t)?[]:[t],l=t=>t.shadowRoot||t,u=(t,e)=>({host:t,target:e}),h="error",g=(t,e,s="debug")=>((t=>"warn"===t||t===h)(s)&&console[s](e,t),t);let d;const{enqueue:f,cleanup:p}=(()=>{const t=new Map,e=new Map;let s;const r=(t,e)=>{try{t()}catch(t){g(t,e,h)}},n=()=>{s=null;for(const[e,s]of t)for(const[t,n]of s)r(n(e),` Effect ${t} on ${e?.localName||"unknown"} failed`);t.clear();for(const t of e.values())r(t,"Cleanup failed");e.clear()},o=t=>(e,r)=>{const o=!t.has(e);t.set(e,r),o&&(s&&cancelAnimationFrame(s),s=requestAnimationFrame(n))};return queueMicrotask(n),{enqueue:(e,s,r)=>{return o((n=e,t.has(n)||t.set(n,new Map),t.get(n)))(s,r);var n},cleanup:o(e)}})(),b=t=>o(t)&&a(t,"set"),m=t=>b(t)||(t=>o(t)&&a(t,"run")&&"effects"in t)(t),y=t=>{const e=()=>(d&&e.effects.add(d),t);return e.effects=new Set,e.set=r=>{const n=t;t=s(r)&&!m(r)?r(t):r,Object.is(t,n)||(t=>{for(const e of t)e.run()})(e.effects)},e},v=t=>{const e=new Set,r=()=>{const n=d;d=r;const o=t(((t,s,r)=>{f(t,s,r),e.add(t)}));s(o)&&p(r,o),d=n};r.run=()=>r(),r.targets=e,r()},w="context-request";class x extends Event{context;callback;subscribe;constructor(t,e,s=!1){super(w,{bubbles:!0,composed:!0}),this.context=t,this.callback=e,this.subscribe=s}}const E=t=>async({host:e,target:r})=>{await e.constructor.registry.whenDefined(r.localName);for(const[n,o]of Object.entries(t))r.set(n,m(o)?o:s(o)?y(o):e.signal(o));return{host:e,target:r}},C=(t,e)=>({host:s,target:r})=>(r.addEventListener(t,e),{host:s,target:r}),N=(t,e)=>({host:s,target:r})=>(r.removeEventListener(t,e),{host:s,target:r}),S=(t,e=t)=>({host:s,target:r})=>(v((()=>{r.dispatchEvent(new CustomEvent(t,{detail:s.get(e),bubbles:!0}))})),{host:s,target:r}),A=t=>[n(t[0])],M=t=>t.map((t=>parseInt(t,10))).filter(Number.isFinite),k=t=>t.map(parseFloat).filter(Number.isFinite),$=t=>t,q=t=>{let e=[];try{e=t.map((t=>JSON.parse(t)))}catch(t){g(t,"Failed to parse JSON",h)}return e},L=(t,e,s,n,o,a,c)=>{const i=a;return t.set(e,o,!1),v((o=>{if(t.has(e)){const a=t.get(e);r(a)?o(s,n,i):o(s,n,(t=>c(t))(a))}})),{host:t,target:s}},F=t=>({host:e,target:s})=>{const r=s.textContent||"",n=t=>e=>()=>{Array.from(e.childNodes).filter(c).forEach((t=>t.remove())),e.append(document.createTextNode(t))};return L(e,t,s,`t-${String(t)}`,r,n(r),n)},O=(t,e=t)=>({host:s,target:r})=>{const n=e=>s=>()=>s[t]=e;return L(s,e,r,`p-${String(t)}`,r[t],n(null),n)},T=(t,e=t)=>({host:s,target:r})=>L(s,e,r,`a-${t}`,r.getAttribute(t),(e=>()=>e.removeAttribute(t)),(e=>s=>()=>s.setAttribute(t,e))),j=(t,e=t)=>({host:s,target:r})=>{const n=e=>s=>()=>s.toggleAttribute(t,e);return L(s,e,r,`a-${t}`,r.hasAttribute(t),n(!1),n)},D=(t,e=t)=>({host:s,target:r})=>L(s,e,r,`c-${t}`,r.classList.contains(t),(e=>()=>e.classList.remove(t)),(e=>s=>()=>s.classList.toggle(t,e))),J=(t,e=t)=>({host:s,target:r})=>L(s,e,r,`s-${t}`,r.style[t],(e=>()=>e.style.removeProperty(t)),(e=>s=>()=>s.style[t]=e)),P=t=>s(t)?P(t()):t;class H extends HTMLElement{static registry=customElements;static attributeMap={};static consumedContexts;static providedContexts;static define(t){try{this.registry.get(t)||this.registry.define(t,this)}catch(e){g(t,e.message,h)}}#t=new Map;self=[u(this,this)];attributeChangedCallback(t,e,r){if(r===e)return;const n=this.constructor.attributeMap[t];this.set(t,s(n)?n(i(r),this,e)[0]:r)}connectedCallback(){const t=this.constructor,e=t.consumedContexts||[];for(const t of e)this.set(String(t),void 0);setTimeout((()=>{for(const t of e)this.dispatchEvent(new x(t,(e=>this.set(String(t),e))))}));const r=t.providedContexts||[];r.length&&this.addEventListener(w,(t=>{const{context:e,callback:n}=t;r.includes(e)&&s(n)&&(t.stopPropagation(),n(this.#t.get(String(e))))}))}disconnectedCallback(){}has(t){return this.#t.has(t)}get(t){return P(this.#t.get(t))}set(t,e,s=!0){if(this.#t.has(t)){if(s){const s=this.#t.get(t);b(s)&&s.set(e)}}else this.#t.set(t,m(e)?e:y(e))}delete(t){return this.#t.delete(t)}signal(t){return this.#t.get(t)}first=(t=>e=>i(l(t).querySelector(e)).map((e=>u(t,e))))(this);all=(t=>e=>Array.from(l(t).querySelectorAll(e)).map((e=>u(t,e))))(this)}export{H as UIElement,A as asBoolean,M as asInteger,q as asJSON,k as asNumber,$ as asString,S as dispatch,v as effect,g as log,i as maybe,N as off,C as on,E as pass,T as setAttribute,O as setProperty,J as setStyle,F as setText,j as toggleAttribute,D as toggleClass}; diff --git a/index.ts b/index.ts index 8aa3406..718d64a 100644 --- a/index.ts +++ b/index.ts @@ -9,7 +9,7 @@ import { setAttribute, setProperty, setStyle, setText, toggleAttribute, toggleCl /** * @name UIElement - * @version 0.8.1 + * @version 0.8.2 */ export { diff --git a/package.json b/package.json index a83d1e8..70ad1cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@efflore/ui-element", - "version": "0.8.1", + "version": "0.8.2", "description": "UIElement - minimal reactive framework based on Web Components", "main": "index.min.js", "types": "types/ui-element.d.ts", diff --git a/src/cause-effect.ts b/src/cause-effect.ts index f93f85d..36597f0 100644 --- a/src/cause-effect.ts +++ b/src/cause-effect.ts @@ -139,7 +139,7 @@ const effect = (fn: EffectCallback) => { activeEffect = n const cleanupFn = fn((element: Element, prop: string, callback: (element: Element) => () => void): void => { enqueue(element, prop, callback) - if (!targets.has(element)) targets.add(element) + targets.add(element) }) if (isFunction(cleanupFn)) cleanup(n, cleanupFn) activeEffect = prev diff --git a/src/core/scheduler.ts b/src/core/scheduler.ts index 29e72eb..57e9e2b 100644 --- a/src/core/scheduler.ts +++ b/src/core/scheduler.ts @@ -1,6 +1,10 @@ -import type { Effect } from '../cause-effect'; import { log, LOG_ERROR } from './log'; +/* === Types === */ + +type UnknownFunction = (...args: unknown[]) => unknown +type ElementFunction = (element: Element) => () => void + /* === Exported Function === */ const scheduler = () => { @@ -8,23 +12,6 @@ const scheduler = () => { const cleanupQueue = new Map() let requestId: number - const requestTick = () => { - if (requestId) cancelAnimationFrame(requestId) - requestId = requestAnimationFrame(flush) - } - - const enqueue = (element: Element, prop: string, fn: (element: Element) => () => void) => { - if (!effectQueue.has(element)) effectQueue.set(element, new Map()) - const elEffects = effectQueue.get(element) - if (!elEffects.has(prop)) elEffects.set(prop, fn) - requestTick() - } - - const cleanup = (effect: Effect, fn: () => void) => { - if (!cleanupQueue.has(effect)) cleanupQueue.set(effect, fn) - requestTick() - } - const run = (fn: () => void, msg: string) => { try { fn() @@ -44,9 +31,30 @@ const scheduler = () => { run(fn, 'Cleanup failed') cleanupQueue.clear() } + + const requestTick = () => { + if (requestId) cancelAnimationFrame(requestId) + requestId = requestAnimationFrame(flush) + } + + const getEffectMap = (key: Element) => { + if (!effectQueue.has(key)) effectQueue.set(key, new Map()) + return effectQueue.get(key) + } + + const addToQueue = (map: Map) => + (key: unknown, fn: UnknownFunction) => { + const more = !map.has(key) + map.set(key, fn) + if (more) requestTick() + } + queueMicrotask(flush) // initial flush when the call stack is empty + return { + enqueue: (element: Element, prop: string, fn: ElementFunction) => addToQueue(getEffectMap(element))(prop, fn), + cleanup: addToQueue(cleanupQueue) + } - return { enqueue, cleanup } } export default scheduler diff --git a/src/ui-element.ts b/src/ui-element.ts index ef31bee..fb60e73 100644 --- a/src/ui-element.ts +++ b/src/ui-element.ts @@ -11,7 +11,7 @@ import { setText, setProperty, setAttribute, toggleAttribute, toggleClass, setSt /* === Types === */ -type AttributeParser = ((value: string[], element: UIElement, old: string | undefined) => T[]) +type AttributeParser = (value: string[], element: UIElement, old: string | undefined) => unknown[] type AttributeMap = Record diff --git a/types/core/scheduler.d.ts b/types/core/scheduler.d.ts index 03d8dcb..5e0765c 100644 --- a/types/core/scheduler.d.ts +++ b/types/core/scheduler.d.ts @@ -1,6 +1,7 @@ -import type { Effect } from '../cause-effect'; +type UnknownFunction = (...args: unknown[]) => unknown; +type ElementFunction = (element: Element) => () => void; declare const scheduler: () => { - enqueue: (element: Element, prop: string, fn: (element: Element) => () => void) => void; - cleanup: (effect: Effect, fn: () => void) => void; + enqueue: (element: Element, prop: string, fn: ElementFunction) => void; + cleanup: (key: unknown, fn: UnknownFunction) => void; }; export default scheduler; diff --git a/types/ui-element.d.ts b/types/ui-element.d.ts index 94c7124..abaa5bb 100644 --- a/types/ui-element.d.ts +++ b/types/ui-element.d.ts @@ -7,7 +7,7 @@ import { type StateMap, pass } from './lib/pass'; import { on, off, dispatch } from './lib/event'; import { asBoolean, asInteger, asJSON, asNumber, asString } from './lib/parse-attribute'; import { setText, setProperty, setAttribute, toggleAttribute, toggleClass, setStyle } from './lib/auto-effects'; -type AttributeParser = ((value: string[], element: UIElement, old: string | undefined) => T[]); +type AttributeParser = (value: string[], element: UIElement, old: string | undefined) => unknown[]; type AttributeMap = Record; /** * Base class for reactive custom elements