Refactor con nombres de variables descriptivos y comentarios en cabeza

This commit is contained in:
2026-04-08 11:54:36 +02:00
parent fc148fdbcc
commit 150cad34d5

353
sw.js
View File

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