140 lines
4.8 KiB
JavaScript
140 lines
4.8 KiB
JavaScript
|
// This file consistently uses `let` keyword instead of `const` for reducing the bundle size.
|
||
|
|
||
|
// Global variables - aliasing some builtin symbols to reduce the bundle size.
|
||
|
let protoOf = Object.getPrototypeOf
|
||
|
let changedStates, derivedStates, curDeps, curNewDerives, alwaysConnectedDom = {isConnected: 1}
|
||
|
let gcCycleInMs = 1000, statesToGc, propSetterCache = {}
|
||
|
let objProto = protoOf(alwaysConnectedDom), funcProto = protoOf(protoOf), _undefined
|
||
|
|
||
|
let addAndScheduleOnFirst = (set, s, f, waitMs) =>
|
||
|
(set ?? (setTimeout(f, waitMs), new Set)).add(s)
|
||
|
|
||
|
let runAndCaptureDeps = (f, deps, arg) => {
|
||
|
let prevDeps = curDeps
|
||
|
curDeps = deps
|
||
|
try {
|
||
|
return f(arg)
|
||
|
} catch (e) {
|
||
|
console.error(e)
|
||
|
return arg
|
||
|
} finally {
|
||
|
curDeps = prevDeps
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let keepConnected = l => l.filter(b => b._dom?.isConnected)
|
||
|
|
||
|
let addStatesToGc = d => statesToGc = addAndScheduleOnFirst(statesToGc, d, () => {
|
||
|
for (let s of statesToGc)
|
||
|
s._bindings = keepConnected(s._bindings),
|
||
|
s._listeners = keepConnected(s._listeners)
|
||
|
statesToGc = _undefined
|
||
|
}, gcCycleInMs)
|
||
|
|
||
|
let stateProto = {
|
||
|
get val() {
|
||
|
curDeps?._getters?.add(this)
|
||
|
return this.rawVal
|
||
|
},
|
||
|
|
||
|
get oldVal() {
|
||
|
curDeps?._getters?.add(this)
|
||
|
return this._oldVal
|
||
|
},
|
||
|
|
||
|
set val(v) {
|
||
|
curDeps?._setters?.add(this)
|
||
|
if (v !== this.rawVal) {
|
||
|
this.rawVal = v
|
||
|
this._bindings.length + this._listeners.length ?
|
||
|
(derivedStates?.add(this), changedStates = addAndScheduleOnFirst(changedStates, this, updateDoms)) :
|
||
|
this._oldVal = v
|
||
|
}
|
||
|
},
|
||
|
}
|
||
|
|
||
|
let state = initVal => ({
|
||
|
__proto__: stateProto,
|
||
|
rawVal: initVal,
|
||
|
_oldVal: initVal,
|
||
|
_bindings: [],
|
||
|
_listeners: [],
|
||
|
})
|
||
|
|
||
|
let bind = (f, dom) => {
|
||
|
let deps = {_getters: new Set, _setters: new Set}, binding = {f}, prevNewDerives = curNewDerives
|
||
|
curNewDerives = []
|
||
|
let newDom = runAndCaptureDeps(f, deps, dom)
|
||
|
newDom = (newDom ?? document).nodeType ? newDom : new Text(newDom)
|
||
|
for (let d of deps._getters)
|
||
|
deps._setters.has(d) || (addStatesToGc(d), d._bindings.push(binding))
|
||
|
for (let l of curNewDerives) l._dom = newDom
|
||
|
curNewDerives = prevNewDerives
|
||
|
return binding._dom = newDom
|
||
|
}
|
||
|
|
||
|
let derive = (f, s = state(), dom) => {
|
||
|
let deps = {_getters: new Set, _setters: new Set}, listener = {f, s}
|
||
|
listener._dom = dom ?? curNewDerives?.push(listener) ?? alwaysConnectedDom
|
||
|
s.val = runAndCaptureDeps(f, deps, s.rawVal)
|
||
|
for (let d of deps._getters)
|
||
|
deps._setters.has(d) || (addStatesToGc(d), d._listeners.push(listener))
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
let add = (dom, ...children) => {
|
||
|
for (let c of children.flat(Infinity)) {
|
||
|
let protoOfC = protoOf(c ?? 0)
|
||
|
let child = protoOfC === stateProto ? bind(() => c.val) :
|
||
|
protoOfC === funcProto ? bind(c) : c
|
||
|
child != _undefined && dom.append(child)
|
||
|
}
|
||
|
return dom
|
||
|
}
|
||
|
|
||
|
let tag = (ns, name, ...args) => {
|
||
|
let [{is, ...props}, ...children] = protoOf(args[0] ?? 0) === objProto ? args : [{}, ...args]
|
||
|
let dom = ns ? document.createElementNS(ns, name, {is}) : document.createElement(name, {is})
|
||
|
for (let [k, v] of Object.entries(props)) {
|
||
|
let getPropDescriptor = proto => proto ?
|
||
|
Object.getOwnPropertyDescriptor(proto, k) ?? getPropDescriptor(protoOf(proto)) :
|
||
|
_undefined
|
||
|
let cacheKey = name + "," + k
|
||
|
let propSetter = propSetterCache[cacheKey] ??= getPropDescriptor(protoOf(dom))?.set ?? 0
|
||
|
let setter = k.startsWith("on") ?
|
||
|
(v, oldV) => {
|
||
|
let event = k.slice(2)
|
||
|
dom.removeEventListener(event, oldV)
|
||
|
dom.addEventListener(event, v)
|
||
|
} :
|
||
|
propSetter ? propSetter.bind(dom) : dom.setAttribute.bind(dom, k)
|
||
|
let protoOfV = protoOf(v ?? 0)
|
||
|
k.startsWith("on") || protoOfV === funcProto && (v = derive(v), protoOfV = stateProto)
|
||
|
protoOfV === stateProto ? bind(() => (setter(v.val, v._oldVal), dom)) : setter(v)
|
||
|
}
|
||
|
return add(dom, children)
|
||
|
}
|
||
|
|
||
|
let handler = ns => ({get: (_, name) => tag.bind(_undefined, ns, name)})
|
||
|
|
||
|
let update = (dom, newDom) => newDom ? newDom !== dom && dom.replaceWith(newDom) : dom.remove()
|
||
|
|
||
|
let updateDoms = () => {
|
||
|
let iter = 0, derivedStatesArray = [...changedStates].filter(s => s.rawVal !== s._oldVal)
|
||
|
do {
|
||
|
derivedStates = new Set
|
||
|
for (let l of new Set(derivedStatesArray.flatMap(s => s._listeners = keepConnected(s._listeners))))
|
||
|
derive(l.f, l.s, l._dom), l._dom = _undefined
|
||
|
} while (++iter < 100 && (derivedStatesArray = [...derivedStates]).length)
|
||
|
let changedStatesArray = [...changedStates].filter(s => s.rawVal !== s._oldVal)
|
||
|
changedStates = _undefined
|
||
|
for (let b of new Set(changedStatesArray.flatMap(s => s._bindings = keepConnected(s._bindings))))
|
||
|
update(b._dom, bind(b.f, b._dom)), b._dom = _undefined
|
||
|
for (let s of changedStatesArray) s._oldVal = s.rawVal
|
||
|
}
|
||
|
|
||
|
export default {
|
||
|
tags: new Proxy(ns => new Proxy(tag, handler(ns)), handler()),
|
||
|
hydrate: (dom, f) => update(dom, bind(f, dom)),
|
||
|
add, state, derive,
|
||
|
}
|