Agrupacion de funciones en linea (visual)

This commit is contained in:
2026-04-08 11:58:00 +02:00
parent 150cad34d5
commit 20022a361c

384
sw.js
View File

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