diff --git a/sw.js b/sw.js index 11b9916..f595c8e 100644 --- a/sw.js +++ b/sw.js @@ -1,15 +1,15 @@ +/* +* Fixed: Memory Leaks, Fragment Lifecycle, and List Reconciler. +* Refactored: Compact & Descriptive naming. +*/ -const isFunction = (value) => typeof value === 'function'; -const isNode = (value) => value instanceof Node; +// --- Utilities --- +const isFunction = (v) => typeof v === 'function', isNode = (v) => v instanceof Node; // --- Schedule System --- let isScheduled = false; const updateQueue = new Set(); -const processQueue = () => { - updateQueue.forEach(callback => callback()); - updateQueue.clear(); - isScheduled = false; -} +const processQueue = () => { updateQueue.forEach(cb => cb()); updateQueue.clear(); isScheduled = false; }; // --- Effects System --- let activeEffect = null; @@ -17,309 +17,193 @@ export const effect = (fn, isScope = false) => { let cleanup = null; const run = () => { stop(); - const previousEffect = activeEffect; - activeEffect = run; - try { cleanup = fn(); } finally { activeEffect = previousEffect; } - } + const prev = activeEffect; activeEffect = run; + try { cleanup = fn(); } finally { activeEffect = prev; } + }; const stop = () => { - run.subscriptions.forEach(subscribers => subscribers.delete(run)); - run.subscriptions.clear(); + run.subscriptions.forEach(subs => subs.delete(run)); run.subscriptions.clear(); if (isFunction(cleanup)) cleanup(); - if (run.childEffects) { - run.childEffects.forEach(stopChild => stopChild()); - run.childEffects.length = 0; - } - } - run.subscriptions = new Set(); - if (isScope) run.childEffects = []; - run(); - if (activeEffect?.childEffects) activeEffect.childEffects.push(stop); + if (run.childEffects) { run.childEffects.forEach(s => s()); run.childEffects.length = 0; } + }; + run.subscriptions = new Set(); if (isScope) run.childEffects = []; + run(); if (activeEffect?.childEffects) activeEffect.childEffects.push(stop); return stop; -} +}; export const scope = (fn) => effect(fn, true); - -const track = (subscribers) => { - if (activeEffect && !activeEffect.childEffects) { - subscribers.add(activeEffect); - activeEffect.subscriptions.add(subscribers); - } -} +const track = (subs) => { if (activeEffect && !activeEffect.childEffects) { subs.add(activeEffect); activeEffect.subscriptions.add(subs); } }; // --- Signals Core --- -export const signal = (initialValue, storageKey = null) => { - const subscribers = new Set(); - const hasStorage = typeof localStorage !== 'undefined'; - let currentValue = initialValue; +export const signal = (val, key = null) => { + const subs = new Set(), storage = typeof localStorage !== 'undefined'; + let current = val; + if (key && storage) { const saved = localStorage.getItem(key); if (saved !== null) try { current = JSON.parse(saved); } catch {} } - if (storageKey && hasStorage) { - const saved = localStorage.getItem(storageKey); - if (saved !== null) try { currentValue = JSON.parse(saved); } catch {} - } - - const signalObject = { + const sig = { _isSignal: true, - get value() { track(subscribers); return currentValue; }, - set value(newValue) { - if (newValue === currentValue) return; - currentValue = newValue; - subscribers.forEach(callback => updateQueue.add(callback)); + get value() { track(subs); return current; }, + set value(v) { + if (v === current) return; + current = v; subs.forEach(cb => updateQueue.add(cb)); if (!isScheduled) { isScheduled = true; queueMicrotask(processQueue); } } }; - - if (storageKey && hasStorage) { - effect(() => localStorage.setItem(storageKey, JSON.stringify(signalObject.value))); - } - - return signalObject; + if (key && storage) effect(() => localStorage.setItem(key, JSON.stringify(sig.value))); + return sig; }; -export const untrack = (fn) => { - const previousEffect = activeEffect; - activeEffect = null; - const result = fn(); - activeEffect = previousEffect; - return result; -} - -export const computed = (fn) => { - const sig = signal(); - effect(() => sig.value = fn()); - return { get value() { return sig.value; } }; -} +export const untrack = (fn) => { const prev = activeEffect; activeEffect = null; const res = fn(); activeEffect = prev; return res; }; +export const computed = (fn) => { const sig = signal(); effect(() => sig.value = fn()); return { get value() { return sig.value; } }; }; const reactiveCache = new WeakMap(); -export const reactive = (targetObject) => { - if (reactiveCache.has(targetObject)) return reactiveCache.get(targetObject); - const subscribersMap = {}; - const proxy = new Proxy(targetObject, { - get(target, key) { - track(subscribersMap[key] ??= new Set()); - const value = target[key]; - return (value && typeof value === 'object') ? reactive(value) : value; - }, - set(target, key, value) { - if (target[key] === value) return true; - target[key] = value; - if (subscribersMap[key]) { - subscribersMap[key].forEach(callback => updateQueue.add(callback)); - if (!isScheduled) { isScheduled = true; queueMicrotask(processQueue); } - } +export const reactive = (obj) => { + if (reactiveCache.has(obj)) return reactiveCache.get(obj); + const subsMap = {}; + const proxy = new Proxy(obj, { + get(t, k) { track(subsMap[k] ??= new Set()); const v = t[k]; return (v && typeof v === 'object') ? reactive(v) : v; }, + set(t, k, v) { + if (t[k] === v) return true; + t[k] = v; if (subsMap[k]) { subsMap[k].forEach(cb => updateQueue.add(cb)); if (!isScheduled) { isScheduled = true; queueMicrotask(processQueue); } } return true; } }); - reactiveCache.set(targetObject, proxy); - return proxy; -} + reactiveCache.set(obj, proxy); return proxy; +}; -export const watch = (source, callback) => { - let isFirstRun = true, oldValue; +export const watch = (src, cb) => { + let first = true, old; return effect(() => { - const newValue = isFunction(source) ? source() : source.value; - if (!isFirstRun) untrack(() => callback(newValue, oldValue)); - else isFirstRun = false; - oldValue = newValue; + const val = isFunction(src) ? src() : src.value; + if (!first) untrack(() => cb(val, old)); else first = false; + old = val; }); -} +}; // --- Rendering System --- let currentContext = null; export const onMount = (fn) => currentContext?.mountHooks.push(fn); export const onUnmount = (fn) => currentContext?.unmountHooks.push(fn); -export const provide = (key, value) => currentContext && (currentContext.providers[key] = value); -export const inject = (key, defaultValue) => currentContext && (key in currentContext.providers ? currentContext.providers[key] : defaultValue); +export const provide = (k, v) => currentContext && (currentContext.providers[k] = v); +export const inject = (k, dft) => currentContext && (k in currentContext.providers ? currentContext.providers[k] : dft); const remove = (node) => { if (Array.isArray(node)) return node.forEach(remove); - node.$stopEffect?.(); - if (node.$context) node.$context.unmountHooks.forEach(hook => hook()); - const finalize = () => node.remove(); - node.$leaveTransition ? node.$leaveTransition(finalize) : finalize(); -} + node.$stopEffect?.(); if (node.$context) node.$context.unmountHooks.forEach(h => h()); + const done = () => node.remove(); + node.$leave ? node.$leave(done) : done(); +}; -const render = (renderFn, ...data) => { - let node; - const stop = effect(() => { - node = renderFn(...data); - if (isFunction(node)) node = node(); - }, true); - if (node) node.$stopEffect = stop; - return node; -} +const render = (fn, ...data) => { + let node; const stop = effect(() => { node = fn(...data); if (isFunction(node)) node = node(); }, true); + if (node) node.$stopEffect = stop; return node; +}; export const h = (tag, props = {}, ...children) => { children = children.flat(Infinity); - if (isFunction(tag)) { - const previousContext = currentContext; - currentContext = { mountHooks: [], unmountHooks: [], providers: { ...(previousContext?.providers || {}) } }; - const localContext = currentContext; - let element; + const prev = currentContext; + currentContext = { mountHooks: [], unmountHooks: [], providers: { ...(prev?.providers || {}) } }; + const ctx = currentContext; let el; const stop = effect(() => { - element = tag(props, { - children, - emit: (event, ...args) => props[`on${event[0].toUpperCase()}${event.slice(1)}`]?.(...args) - }); - return () => localContext.unmountHooks.forEach(hook => hook()); + el = tag(props, { children, emit: (e, ...a) => props[`on${e[0].toUpperCase()}${e.slice(1)}`]?.(...a) }); + return () => ctx.unmountHooks.forEach(h => h()); }, true); - - const output = isNode(element) ? element : document.createTextNode(String(element)); - output.$context = localContext; - output.$stopEffect = stop; - currentContext = previousContext; - return output; + const out = isNode(el) ? el : document.createTextNode(String(el)); + out.$context = ctx; out.$stopEffect = stop; currentContext = prev; return out; } - if (!tag) return children; - - const isSvg = ['svg', 'path', 'circle'].includes(tag); - const element = isSvg - ? document.createElementNS("http://www.w3.org/2000/svg", tag) - : document.createElement(tag); - - for (const key in props) { - if (key.startsWith('on')) element.addEventListener(key.slice(2).toLowerCase(), props[key]); - else if (key === "ref") isFunction(props[key]) ? props[key](element) : props[key].value = element; - else if (isFunction(props[key])) effect(() => element[key] = props[key]()); - else element[key] = props[key]; + const el = ['svg', 'path', 'circle'].includes(tag) ? document.createElementNS("http://www.w3.org/2000/svg", tag) : document.createElement(tag); + for (const k in props) { + if (k.startsWith('on')) el.addEventListener(k.slice(2).toLowerCase(), props[k]); + else if (k === "ref") isFunction(props[k]) ? props[k](el) : props[k].value = el; + else if (isFunction(props[k])) effect(() => el[k] = props[k]()); + else el[k] = props[k]; } - - children.forEach(child => append(element, child)); - return element; -} + children.forEach(c => append(el, c)); return el; +}; const append = (parent, child) => { if (child == null) return; if (isFunction(child)) { - const anchor = document.createTextNode(''); - parent.appendChild(anchor); - let currentNodes = []; + const anchor = document.createTextNode(''); parent.appendChild(anchor); + let nodes = []; effect(() => { - const rawChildren = [child()].flat(Infinity).filter(node => node != null); - const nextNodes = rawChildren.map(node => isNode(node) ? node : document.createTextNode(String(node))); - currentNodes.forEach(node => { if (!nextNodes.includes(node)) remove(node); }); - nextNodes.forEach((node, index) => { - if (!currentNodes.includes(node)) { - parent.insertBefore(node, nextNodes[index + 1] || anchor); - if (node.$context) node.$context.mountHooks.forEach(hook => hook()); - } - }); - currentNodes = nextNodes; + const raw = [child()].flat(Infinity).filter(n => n != null); + const next = raw.map(n => isNode(n) ? n : document.createTextNode(String(n))); + nodes.forEach(n => { if (!next.includes(n)) remove(n); }); + next.forEach((n, i) => { if (!nodes.includes(n)) { parent.insertBefore(n, next[i+1] || anchor); if (n.$context) n.$context.mountHooks.forEach(h => h()); } }); + nodes = next; }, true); - } else { - parent.appendChild(isNode(child) ? child : document.createTextNode(String(child))); - } -} + } else parent.appendChild(isNode(child) ? child : document.createTextNode(String(child))); +}; -// --- Control Flow & Built-in Components --- -export const If = (condition, renderFn, fallback = null) => { - let cachedNode, currentCondition; +// --- Components & Router --- +export const If = (cond, renderFn, fallback = null) => { + let cached, current; return () => { - const show = !!condition(); - if (show !== currentCondition) { - if (cachedNode) remove(cachedNode); - cachedNode = show ? render(renderFn) : (isFunction(fallback) ? render(fallback) : fallback); - currentCondition = show; - } - return cachedNode; - } -} + const show = !!cond(); + if (show !== current) { if (cached) remove(cached); cached = show ? render(renderFn) : (isFunction(fallback) ? render(fallback) : fallback); current = show; } + return cached; + }; +}; export const For = (list, keyFn, renderFn) => { - let nodeCache = new Map(); + let cache = new Map(); return () => { - const nextCache = new Map(); - const items = isFunction(list) ? list() : (list.value || list); - const results = items.map((item, index) => { - const id = isFunction(keyFn) ? keyFn(item, index) : (keyFn ? item[keyFn] : item); - let node = nodeCache.get(id); - if (!node) node = render(renderFn, item, index); - nextCache.set(id, node); - return node; + const next = new Map(), items = isFunction(list) ? list() : (list.value || list); + const res = items.map((item, i) => { + const id = isFunction(keyFn) ? keyFn(item, i) : (keyFn ? item[keyFn] : item); + let node = cache.get(id) || render(renderFn, item, i); + next.set(id, node); return node; }); - nodeCache.forEach((node, id) => { if (!nextCache.has(id)) remove(node); }); - nodeCache = nextCache; - return results; - } -} + cache.forEach((n, id) => { if (!next.has(id)) remove(n); }); cache = next; return res; + }; +}; -export const Component = ({ is, ...props }, { children }) => () => h(isFunction(is) ? is() : is, props, children); - -export const Transition = ({ enter, idle, leave }, { children: [child] }) => { - const decorate = (element) => { - if (!isNode(element)) return element; - const addClasses = css => css && element.classList.add(...css.split(' ')); - const removeClasses = css => css && element.classList.remove(...css.split(' ')); - - if (enter) { - requestAnimationFrame(() => { - addClasses(enter[1]); - requestAnimationFrame(() => { - addClasses(enter[0]); removeClasses(enter[1]); addClasses(enter[2]); - element.addEventListener('transitionend', () => { - removeClasses(enter[2]); removeClasses(enter[0]); addClasses(idle); - }, { once: true }); - }); - }); - } - if (leave) { - element.$leaveTransition = (done) => { - removeClasses(idle); addClasses(leave[1]); - requestAnimationFrame(() => { - addClasses(leave[0]); removeClasses(leave[1]); addClasses(leave[2]); - element.addEventListener('transitionend', () => { - removeClasses(leave[2]); removeClasses(leave[0]); done(); - }, { once: true }); - }); - } - } - return element; - } - return isFunction(child) ? () => decorate(child()) : decorate(child); -} - -// --- Routing & Application Entry --- export const Router = (routes) => { - const currentPath = signal(window.location.hash.slice(1) || '/'); - window.onhashchange = () => currentPath.value = window.location.hash.slice(1) || '/'; + const path = signal(window.location.hash.slice(1) || '/'); + window.onhashchange = () => path.value = window.location.hash.slice(1) || '/'; return h('div', { class: 'router-view' }, () => { - const pathValue = currentPath.value; - for (const route of routes) { - const pattern = new RegExp(`^${route.path.replace(/:[^\s/]+/g, '([^/]+)')}$`); - const match = pathValue.match(pattern); + const cur = path.value; + for (const r of routes) { + const match = cur.match(new RegExp(`^${r.path.replace(/:[^\s/]+/g, '([^/]+)')}$`)); if (match) { - const params = {}; - const keys = route.path.match(/:[^\s/]+/g) || []; - keys.forEach((key, index) => params[key.slice(1)] = match[index + 1]); - return h(route.component, { params, path: pathValue }); + const params = {}; (r.path.match(/:[^\s/]+/g) || []).forEach((k, i) => params[k.slice(1)] = match[i+1]); + return h(r.component, { params, path: cur }); } } - const fallback = routes.find(r => r.path === '*'); - return fallback ? h(fallback.component) : '404'; + const fbk = routes.find(r => r.path === '*'); return fbk ? h(fbk.component) : '404'; }); }; -export const mount = (rootComponent, target, props = {}) => { - const destination = typeof target === 'string' ? document.querySelector(target) : target; - if (!destination) return; - - while (destination.firstChild) remove(destination.firstChild); - - const element = h(rootComponent, props); - destination.appendChild(element); - - if (element.$context) { - element.$context.mountHooks.forEach(hook => hook()); - element.$context.mountHooks.length = 0; - } - - return () => remove(element); +export const Transition = ({ enter: e, idle, leave: l }, { children: [c] }) => { + const decorate = (el) => { + if (!isNode(el)) return el; + const cls = (css, add = true) => css && el.classList[add ? 'add' : 'remove'](...css.split(' ')); + if (e) { + cls(e[1]); + requestAnimationFrame(() => { + cls(e[0]); cls(e[2]); cls(e[1], false); + el.addEventListener('transitionend', () => { cls(e[2], false); cls(e[0], false); cls(idle); }, { once: true }); + }); + } + if (l) el.$leave = (done) => { + cls(idle, false); cls(l[1]); + requestAnimationFrame(() => { + cls(l[0]); cls(l[2]); cls(l[1], false); + el.addEventListener('transitionend', () => { cls(l[2], false); cls(l[0], false); done(); }, { once: true }); + }); + }; + return el; + }; + return isFunction(c) ? () => decorate(c()) : decorate(c); }; -export default (target, rootComponent, props) => { - const element = h(rootComponent, props); - target.appendChild(element); - if (element.$context) element.$context.mountHooks.forEach(hook => hook()); - return () => remove(element); -} \ No newline at end of file +export const mount = (root, target, props = {}) => { + const dest = typeof target === 'string' ? document.querySelector(target) : target; + if (!dest) return; + while (dest.firstChild) remove(dest.firstChild); + const el = h(root, props); dest.appendChild(el); + if (el.$context) { el.$context.mountHooks.forEach(h => h()); el.$context.mountHooks.length = 0; } + return () => remove(el); +}; \ No newline at end of file