209 lines
8.9 KiB
JavaScript
209 lines
8.9 KiB
JavaScript
/*
|
|
* Fixed: Memory Leaks, Fragment Lifecycle, and List Reconciler.
|
|
* Refactored: Compact & Descriptive naming.
|
|
*/
|
|
|
|
// --- 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(cb => cb()); updateQueue.clear(); isScheduled = false; };
|
|
|
|
// --- Effects System ---
|
|
let activeEffect = null;
|
|
export const effect = (fn, isScope = false) => {
|
|
let cleanup = null;
|
|
const run = () => {
|
|
stop();
|
|
const prev = activeEffect; activeEffect = run;
|
|
try { cleanup = fn(); } finally { activeEffect = prev; }
|
|
};
|
|
const stop = () => {
|
|
run.subscriptions.forEach(subs => subs.delete(run)); run.subscriptions.clear();
|
|
if (isFunction(cleanup)) cleanup();
|
|
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 = (subs) => { if (activeEffect && !activeEffect.childEffects) { subs.add(activeEffect); activeEffect.subscriptions.add(subs); } };
|
|
|
|
// --- Signals Core ---
|
|
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 {} }
|
|
|
|
const sig = {
|
|
_isSignal: true,
|
|
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 (key && storage) effect(() => localStorage.setItem(key, JSON.stringify(sig.value)));
|
|
return sig;
|
|
};
|
|
|
|
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 = (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(obj, proxy); return proxy;
|
|
};
|
|
|
|
export const watch = (src, cb) => {
|
|
let first = true, old;
|
|
return effect(() => {
|
|
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 = (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(h => h());
|
|
const done = () => node.remove();
|
|
node.$leave ? node.$leave(done) : done();
|
|
};
|
|
|
|
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 prev = currentContext;
|
|
currentContext = { mountHooks: [], unmountHooks: [], providers: { ...(prev?.providers || {}) } };
|
|
const ctx = currentContext; let el;
|
|
const stop = effect(() => {
|
|
el = tag(props, { children, emit: (e, ...a) => props[`on${e[0].toUpperCase()}${e.slice(1)}`]?.(...a) });
|
|
return () => ctx.unmountHooks.forEach(h => h());
|
|
}, true);
|
|
const out = isNode(el) ? el : document.createTextNode(String(el));
|
|
out.$context = ctx; out.$stopEffect = stop; currentContext = prev; return out;
|
|
}
|
|
if (!tag) return children;
|
|
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(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 nodes = [];
|
|
effect(() => {
|
|
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)));
|
|
};
|
|
|
|
// --- Components & Router ---
|
|
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) : (isFunction(fallback) ? render(fallback) : fallback); current = show; }
|
|
return cached;
|
|
};
|
|
};
|
|
|
|
export const For = (list, keyFn, renderFn) => {
|
|
let cache = new Map();
|
|
return () => {
|
|
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;
|
|
});
|
|
cache.forEach((n, id) => { if (!next.has(id)) remove(n); }); cache = next; return res;
|
|
};
|
|
};
|
|
|
|
export const Router = (routes) => {
|
|
const path = signal(window.location.hash.slice(1) || '/');
|
|
window.onhashchange = () => path.value = window.location.hash.slice(1) || '/';
|
|
return h('div', { class: 'router-view' }, () => {
|
|
const cur = path.value;
|
|
for (const r of routes) {
|
|
const match = cur.match(new RegExp(`^${r.path.replace(/:[^\s/]+/g, '([^/]+)')}$`));
|
|
if (match) {
|
|
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 fbk = routes.find(r => r.path === '*'); return fbk ? h(fbk.component) : '404';
|
|
});
|
|
};
|
|
|
|
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 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);
|
|
}; |