fase1
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* Sigwork - Optimized & Leak-free
|
* Sigwork v1.1 - [Sig]nal-based Frontend Frame[work]
|
||||||
|
* Fixed: Memory Leaks, Fragment Lifecycle, and List Reconciler.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const isFn = (v) => typeof v === 'function';
|
const isFn = (v) => typeof v === 'function';
|
||||||
const isNode = (v) => v instanceof Node;
|
const isNode = (v) => v instanceof Node;
|
||||||
|
|
||||||
// --- Sistema de Programación ---
|
// --- Schedule System ---
|
||||||
let isScheduled = false;
|
let isScheduled = false;
|
||||||
const queue = new Set();
|
const queue = new Set();
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
@@ -14,12 +15,12 @@ const tick = () => {
|
|||||||
isScheduled = false;
|
isScheduled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Efectos y Alcance ---
|
// --- Effects System ---
|
||||||
let activeEffect = null;
|
let activeEffect = null;
|
||||||
export const effect = (fn, is_scope = false) => {
|
export const effect = (fn, is_scope = false) => {
|
||||||
let cleanup;
|
let cleanup = null;
|
||||||
const run = () => {
|
const run = () => {
|
||||||
stop(); // Limpia suscripciones y ejecuciones previas
|
stop(); // Limpia antes de re-ejecutar
|
||||||
const prev = activeEffect;
|
const prev = activeEffect;
|
||||||
activeEffect = run;
|
activeEffect = run;
|
||||||
try { cleanup = fn(); } finally { activeEffect = prev; }
|
try { cleanup = fn(); } finally { activeEffect = prev; }
|
||||||
@@ -33,11 +34,9 @@ export const effect = (fn, is_scope = false) => {
|
|||||||
run.c.length = 0;
|
run.c.length = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
run.e = new Set(); // Suscripciones (Signals)
|
run.e = new Set();
|
||||||
if (is_scope) run.c = []; // Efectos hijos
|
if (is_scope) run.c = [];
|
||||||
run();
|
run();
|
||||||
|
|
||||||
// Registro en el padre para evitar fugas
|
|
||||||
if (activeEffect?.c) activeEffect.c.push(stop);
|
if (activeEffect?.c) activeEffect.c.push(stop);
|
||||||
return stop;
|
return stop;
|
||||||
}
|
}
|
||||||
@@ -45,78 +44,130 @@ export const effect = (fn, is_scope = false) => {
|
|||||||
export const scope = f => effect(f, true);
|
export const scope = f => effect(f, true);
|
||||||
|
|
||||||
const track = (subs) => {
|
const track = (subs) => {
|
||||||
if (activeEffect) {
|
if (activeEffect && !activeEffect.c) {
|
||||||
subs.add(activeEffect);
|
subs.add(activeEffect);
|
||||||
activeEffect.e.add(subs);
|
activeEffect.e.add(subs);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// --- Signals ---
|
// --- Signals Core ---
|
||||||
export const signal = (value) => {
|
export const signal = (value) => {
|
||||||
const subs = new Set();
|
const subs = new Set();
|
||||||
return {
|
return {
|
||||||
get value() { track(subs); return value; },
|
get value() { track(subs); return value; },
|
||||||
set value(nv) {
|
set value(newValue) {
|
||||||
if (nv === value) return;
|
if (newValue === value) return;
|
||||||
value = nv;
|
value = newValue;
|
||||||
subs.forEach(fn => queue.add(fn));
|
subs.forEach(fn => queue.add(fn));
|
||||||
if (!isScheduled) { isScheduled = true; queueMicrotask(tick); }
|
if (!isScheduled) { isScheduled = true; queueMicrotask(tick); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const untrack = (fn) => {
|
||||||
|
const prev = activeEffect;
|
||||||
|
activeEffect = null;
|
||||||
|
const result = fn();
|
||||||
|
activeEffect = prev;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export const computed = (fn) => {
|
export const computed = (fn) => {
|
||||||
const sig = signal();
|
const sig = signal();
|
||||||
effect(() => sig.value = fn());
|
effect(() => sig.value = fn());
|
||||||
return { get value() { return sig.value; } };
|
return { get value() { return sig.value; } };
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Rendering Core ---
|
const reactiveCache = new WeakMap();
|
||||||
|
export const reactive = (obj) => {
|
||||||
|
if (reactiveCache.has(obj)) return reactiveCache.get(obj);
|
||||||
|
const subs = {};
|
||||||
|
const proxy = new Proxy(obj, {
|
||||||
|
get(t, key) {
|
||||||
|
track(subs[key] ??= new Set());
|
||||||
|
const val = t[key];
|
||||||
|
return (val && typeof val === 'object') ? reactive(val) : val;
|
||||||
|
},
|
||||||
|
set(t, key, val) {
|
||||||
|
if (t[key] === val) return true;
|
||||||
|
t[key] = val;
|
||||||
|
if (subs[key]) {
|
||||||
|
subs[key].forEach(fn => queue.add(fn));
|
||||||
|
if (!isScheduled) { isScheduled = true; queueMicrotask(tick); }
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reactiveCache.set(obj, proxy);
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const watch = (source, cb) => {
|
||||||
|
let first = true, oldValue;
|
||||||
|
return effect(() => {
|
||||||
|
const newValue = isFn(source) ? source() : source.value;
|
||||||
|
if (!first) untrack(() => cb(newValue, oldValue));
|
||||||
|
else first = false;
|
||||||
|
oldValue = newValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Rendering System ---
|
||||||
let context = null;
|
let context = null;
|
||||||
export const onMount = (fn) => context?.m.push(fn);
|
export const onMount = (fn) => context?.m.push(fn);
|
||||||
export const onUnmount = (fn) => context?.u.push(fn);
|
export const onUnmount = (fn) => context?.u.push(fn);
|
||||||
|
export const provide = (key, value) => context && (context.p[key] = value);
|
||||||
|
export const inject = (key, dft) => context && (key in context.p ? context.p[key] : dft);
|
||||||
|
|
||||||
const remove = (node) => {
|
const remove = (node) => {
|
||||||
if (Array.isArray(node)) return node.forEach(remove);
|
if (Array.isArray(node)) return node.forEach(remove);
|
||||||
node.$s?.(); // Detener efectos del nodo
|
node.$s?.();
|
||||||
if (node.$c) node.$c.u.forEach(f => f()); // Ejecutar onUnmount
|
if (node.$c) node.$c.u.forEach(f => f());
|
||||||
const done = () => node.remove();
|
const done = () => node.remove();
|
||||||
node.$l ? node.$l(done) : done();
|
node.$l ? node.$l(done) : done();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const render = (fn, ...data) => {
|
||||||
|
let node;
|
||||||
|
const stop = effect(() => {
|
||||||
|
node = fn(...data);
|
||||||
|
if (isFn(node)) node = node();
|
||||||
|
}, true);
|
||||||
|
if (node) node.$s = stop;
|
||||||
|
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 (isFn(tag)) {
|
||||||
const prev = context;
|
const prev = context;
|
||||||
const ctx = context = { m: [], u: [], p: { ...(prev?.p || {}) } };
|
context = { m: [], u: [], p: { ...(prev?.p || {}) } };
|
||||||
let res;
|
const ctx = context;
|
||||||
|
let el;
|
||||||
const stop = effect(() => {
|
const stop = effect(() => {
|
||||||
res = tag(props, { children, emit: (e, ...a) => props[`on${e[0].toUpperCase()}${e.slice(1)}`]?.(...a) });
|
el = tag(props, { children, emit: (evt, ...args) => props[`on${evt[0].toUpperCase()}${evt.slice(1)}`]?.(...args) });
|
||||||
return () => ctx.u.forEach(f => f());
|
return () => ctx.u.forEach(f => f());
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
// Si el componente devuelve un array, necesitamos un ancla para colgar el ciclo de vida
|
// Normalización para asegurar que el nodo tenga metadatos
|
||||||
const out = isNode(res) ? res : document.createTextNode(String(res));
|
const out = isNode(el) ? el : document.createTextNode(String(el));
|
||||||
out.$c = ctx;
|
out.$c = ctx;
|
||||||
out.$s = stop;
|
out.$s = stop;
|
||||||
context = prev;
|
context = prev;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tag) return children; // Fragment (manejo simple)
|
if (!tag) return children;
|
||||||
|
|
||||||
const isSvg = tag === 'svg' || tag === 'path' || tag === 'circle'; // Simplificado
|
const isSvg = tag === 'svg' || tag === 'path' || tag === 'circle';
|
||||||
const el = isSvg
|
const el = isSvg ? document.createElementNS("http://www.w3.org/2000/svg", tag) : document.createElement(tag);
|
||||||
? document.createElementNS("http://www.w3.org/2000/svg", tag)
|
|
||||||
: document.createElement(tag);
|
|
||||||
|
|
||||||
for (const key in props) {
|
for (const key in props) {
|
||||||
const val = props[key];
|
if (key.startsWith('on')) el.addEventListener(key.slice(2).toLowerCase(), props[key]);
|
||||||
if (key.startsWith('on')) el.addEventListener(key.slice(2).toLowerCase(), val);
|
else if (key === "ref") isFn(props[key]) ? props[key](el) : props[key].value = el;
|
||||||
else if (key === "ref") isFn(val) ? val(el) : (val.value = el);
|
else if (isFn(props[key])) effect(() => el[key] = props[key]());
|
||||||
else if (isFn(val)) effect(() => el[key] = val()); // Atribución reactiva
|
else el[key] = props[key];
|
||||||
else el[key] = val;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
children.forEach(child => append(el, child));
|
children.forEach(child => append(el, child));
|
||||||
@@ -130,13 +181,9 @@ const append = (parent, child) => {
|
|||||||
parent.appendChild(anchor);
|
parent.appendChild(anchor);
|
||||||
let nodes = [];
|
let nodes = [];
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const raw = [child()].flat(Infinity).filter(v => v != null);
|
const raw = [child()].flat(Infinity).filter(n => n != null);
|
||||||
const newNodes = raw.map(n => isNode(n) ? n : document.createTextNode(String(n)));
|
const newNodes = raw.map(n => isNode(n) ? n : document.createTextNode(String(n)));
|
||||||
|
|
||||||
// Cleanup de nodos antiguos que ya no están
|
|
||||||
nodes.forEach(n => { if (!newNodes.includes(n)) remove(n); });
|
nodes.forEach(n => { if (!newNodes.includes(n)) remove(n); });
|
||||||
|
|
||||||
// Reconciliación simple
|
|
||||||
newNodes.forEach((n, i) => {
|
newNodes.forEach((n, i) => {
|
||||||
if (!nodes.includes(n)) {
|
if (!nodes.includes(n)) {
|
||||||
parent.insertBefore(n, newNodes[i+1] || anchor);
|
parent.insertBefore(n, newNodes[i+1] || anchor);
|
||||||
@@ -150,22 +197,76 @@ const append = (parent, child) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
// --- Helpers & Built-in ---
|
||||||
|
export const If = (cond, renderFn, fallback = null) => {
|
||||||
|
let cached, current;
|
||||||
|
return () => {
|
||||||
|
const show = !!cond();
|
||||||
|
if (show !== current) {
|
||||||
|
if (cached) remove(cached);
|
||||||
|
cached = show ? render(renderFn) : (isFn(fallback) ? render(fallback) : fallback);
|
||||||
|
current = show;
|
||||||
|
}
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const For = (list, key, renderFn) => {
|
export const For = (list, key, renderFn) => {
|
||||||
let cache = new Map();
|
let cache = new Map();
|
||||||
return () => {
|
return () => {
|
||||||
const next = new Map();
|
const next = new Map();
|
||||||
const items = isFn(list) ? list() : list.value || list;
|
const items = isFn(list) ? list() : (list.value || list);
|
||||||
const res = items.map((item, i) => {
|
const res = items.map((item, i) => {
|
||||||
const id = isFn(key) ? key(item, i) : (key ? item[key] : item);
|
const id = isFn(key) ? key(item, i) : (key ? item[id] : item);
|
||||||
let node = cache.get(id);
|
let node = cache.get(id);
|
||||||
if (!node) node = h(() => renderFn(item, i));
|
if (!node) node = render(renderFn, item, i);
|
||||||
next.set(id, node);
|
next.set(id, node);
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
// Limpiar nodos que salen de la lista
|
|
||||||
cache.forEach((node, id) => { if (!next.has(id)) remove(node); });
|
cache.forEach((node, id) => { if (!next.has(id)) remove(node); });
|
||||||
cache = next;
|
cache = next;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Component = ({ is, ...props }, { children }) => () => h(isFn(is) ? is() : is, props, children);
|
||||||
|
|
||||||
|
export const Transition = ({ enter: e, idle, leave: l }, { children: [c] }) => {
|
||||||
|
const decorate = (el) => {
|
||||||
|
if (!isNode(el)) return el;
|
||||||
|
const addClass = c => c && el.classList.add(...c.split(' '));
|
||||||
|
const removeClass = c => c && el.classList.remove(...c.split(' '));
|
||||||
|
|
||||||
|
if (e) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
addClass(e[1]);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
addClass(e[0]); removeClass(e[1]); addClass(e[2]);
|
||||||
|
el.addEventListener('transitionend', () => {
|
||||||
|
removeClass(e[2]); removeClass(e[0]); addClass(idle);
|
||||||
|
}, { once: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (l) {
|
||||||
|
el.$l = (done) => {
|
||||||
|
removeClass(idle); addClass(l[1]);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
addClass(l[0]); removeClass(l[1]); addClass(l[2]);
|
||||||
|
el.addEventListener('transitionend', () => {
|
||||||
|
removeClass(l[2]); removeClass(l[0]); done();
|
||||||
|
}, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
return isFn(c) ? () => decorate(c()) : decorate(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (target, root, props) => {
|
||||||
|
const el = h(root, props);
|
||||||
|
target.appendChild(el);
|
||||||
|
if (el.$c) el.$c.m.forEach(f => f());
|
||||||
|
return () => remove(el);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user