completely rewrite reactivity

This commit is contained in:
CoolElectronics 2024-02-14 20:43:25 -05:00
parent 444d7d7f58
commit 2d577a6a99
No known key found for this signature in database
GPG key ID: F63593D168636C50
2 changed files with 211 additions and 190 deletions

View file

@ -1,6 +1,5 @@
function Counter(a) { function Counter() {
console.log(a); this.css = css`
let css = styled.new`
self { self {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -25,7 +24,7 @@ function Counter(a) {
this.counter ??= 0; this.counter ??= 0;
return ( return (
<div css={css} class="box"> <div class="box">
<h1>Counter</h1> <h1>Counter</h1>
<p> <p>
Value: {use(this.counter)} Value: {use(this.counter)}
@ -37,89 +36,98 @@ function Counter(a) {
</div> </div>
) )
} }
//
function ToDoList() { // function ToDoList() {
let css = styled.new` // let css = styled.new`
self { // self {
color: #e0def4; // color: #e0def4;
display:flex; // display:flex;
flex-direction:column; // flex-direction:column;
} // }
//
.todoitem { // .todoitem {
display:flex; // display:flex;
} // }
` // `
//
this.tasks = []; // this.tasks = [];
this.text = "Enter a task here..."; // this.text = "Enter a task here...";
//
let addTask = () => { // let addTask = () => {
if (!this.text) return; // if (!this.text) return;
this.tasks = [...this.tasks, this.text]; // this.tasks = [...this.tasks, this.text];
this.text = ""; // this.text = "";
}; // };
//
return ( // return (
<div class="box" css={css}> // <div class="box" css={css}>
<div> // <div>
<input bind:value={use(this.text)} on:change={() => addTask()} /> // <input bind:value={use(this.text)} on:change={() => addTask()} />
<button on:click={() => addTask()}>Add Task</button> // <button on:click={() => addTask()}>Add Task</button>
</div> // </div>
<div for={use(this.tasks)} do={(task, i) => // <div for={use(this.tasks)} do={(task, i) =>
<div class="todoitem"> // <div class="todoitem">
{task} // {task}
<button on:click={() => { // <button on:click={() => {
this.tasks.splice(i, 1) // this.tasks.splice(i, 1)
this.tasks = this.tasks // this.tasks = this.tasks
}}>Delete</button> // }}>Delete</button>
</div> // </div>
} /> // } />
</div> // </div>
) // )
} // }
//
//
function Index() { // function Index() {
let css = styled.new` // let css = styled.new`
h1 { // h1 {
font-size: 40px; // font-size: 40px;
text-align:center; // text-align:center;
} // }
p { // p {
text-align:center; // text-align:center;
font-size:15px; // font-size:15px;
} // }
div { // div {
/* margin-bottom:3em; */ // /* margin-bottom:3em; */
} // }
examples { // examples {
display: flex; // display: flex;
justify-content: center; // justify-content: center;
flex-direction: column; // flex-direction: column;
} // }
`; // `;
//
this.c = 5; // this.c = 5;
//
this.counterobj; // this.counterobj;
//
return ( // return (
<div className={"as"}> // <div className={"as"}>
<div> // <Counter />
<h1>AliceJS Examples</h1> // {/* <div> */}
<p>Some examples of AliceJS components. Code is in examples/</p> // {/* <h1>AliceJS Examples</h1> */}
</div> // {/* <p>Some examples of AliceJS components. Code is in examples/</p> */}
<examples> // {/* </div> */}
<Counter a="b" bind:this={use(this.counterobj)} bind:counter={use(this.c)} /> // {/* <examples> */}
<ToDoList /> // {/* <Counter a="b" bind:this={use(this.counterobj)} bind:counter={use(this.c)} /> */}
</examples> // {/* <ToDoList /> */}
stuff: {use(this.counterobj.counter)} // {/* </examples> */}
<button on:click={() => this.counterobj.counter++}>as</button> // {/* stuff: {use(this.counterobj.counter)} */}
</div> // {/* <button on:click={() => this.counterobj.counter++}>as</button> */}
); // </div>
} // );
// }
window.addEventListener("load", () => { window.addEventListener("load", () => {
document.body.appendChild(<Index />); document.body.appendChild(<Counter />);
});
let a = stateful({ b: stateful({ c: stateful({ d: 0 }) }), array: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] }) as any;
let r = use(a.array[a.b.c.d][a.b.c.d]);
handle(r, v => {
console.log(v);
}); });

205
js.js
View file

@ -1,52 +1,77 @@
// The global list of "references", as captured by the proxy in stateful // whether to return the true value from a stateful object or a "trap" containing the pointer
let __reference_stack = []; let __use_trap = false;
// We add some extra properties into various objects throughout, better to use symbols and not interfere // We add some extra properties into various objects throughout, better to use symbols and not interfere
let ALICEJS_REFERENCES_MAPPING = Symbol(); let USE_MAPFN = Symbol();
let ALICEJS_REFERENCES_MARKER = Symbol();
let ALICEJS_STATEFUL_LISTENERS = Symbol();
// Say you have some code like // Say you have some code like
//// let state = stateful({ //// let state = stateful({
//// a: 1 //// a: stateful({
//// b: 1
//// }) //// })
//// let elm = <p>{window.use(state.a)}</p> //// })
//// let elm = <p>{window.use(state.a.b)}</p>
// //
// According to the standard, the order of events is as follows: // According to the standard, the order of events is as follows:
// - the getter for window.use gets called, setting __reference_stack to an empty list // - the getter for window.use gets called, setting __use_trap true
// - the proxy for state.a is triggered, pushing { state, "a", Proxy(state) } onto __reference_stack // - the proxy for state.a is triggered and instead of returning the normal value it returns the trap
// - the function that the getter returns is called, popping everything off the stack // - the trap proxy is triggered, storing ["a", "b"] as the order of events
// - the JSX factory h() is now passed the *reference* of state.a, not the value // - the function that the getter of `use` returns is called, setting __use_trap to false and restoring order
// - the JSX factory h() is now passed the trap, which essentially contains a set of pointers pointing to the theoretical value of b
// - with the setter on the stateful proxy, we can listen to any change in any of the nested layers and call whatever listeners registered
// - the result is full intuitive reactivity with minimal overhead
Object.defineProperty(window, "use", { Object.defineProperty(window, "use", {
get: () => { get: () => {
__reference_stack = []; __use_trap = true;
return (_sink, mapping) => { return (ptr, mapping) => {
let references = __reference_stack; __use_trap = false;
__reference_stack = []; if (mapping) ptr[USE_MAPFN] = mapping;
return ptr;
references[ALICEJS_REFERENCES_MARKER] = true;
if (mapping) references[ALICEJS_REFERENCES_MAPPING] = mapping;
return references;
}; };
} }
}); });
Object.assign(window, { isAJSReferences, h, stateful, handle, useValue }); Object.assign(window, { isDLPtr, h, stateful, handle, useValue });
const TARGET = Symbol();
const PROXY = Symbol();
const STEPS = Symbol();
const LISTENERS = Symbol();
const TRAPS = new Map;
// This wraps the target in a proxy, doing 2 things: // This wraps the target in a proxy, doing 2 things:
// - whenever a property is accessed, update the reference stack // - whenever a property is accessed, return a "trap" that catches and records accessors
// - whenever a property is set, notify the subscribed listeners // - whenever a property is set, notify the subscribed listeners
// This is what makes our "pass-by-reference" magic work // This is what makes our "pass-by-reference" magic work
export function stateful(target) { export function stateful(target) {
target[ALICEJS_STATEFUL_LISTENERS] = []; target[LISTENERS] = [];
target[TARGET] = target;
const proxy = new Proxy(target, { const proxy = new Proxy(target, {
get(target, property, proxy) { get(target, property, proxy) {
__reference_stack.push({ target, property, proxy }); if (__use_trap) {
let sym = Symbol();
let trap = new Proxy({
[TARGET]: target,
[PROXY]: proxy,
[STEPS]: [property],
[Symbol.toPrimitive]: () => sym,
}, {
get(target, property) {
if (property === TARGET || property === PROXY || property === STEPS || property === Symbol.toPrimitive) return target[property];
property = TRAPS.get(property) || property;
target[STEPS].push(property);
return trap;
}
});
TRAPS.set(sym, trap);
return trap;
}
return Reflect.get(target, property, proxy); return Reflect.get(target, property, proxy);
}, },
set(target, property, val) { set(target, property, val) {
let trap = Reflect.set(target, property, val); let trap = Reflect.set(target, property, val);
for (const listener of target[ALICEJS_STATEFUL_LISTENERS]) { for (const listener of target[LISTENERS]) {
listener(target, property, val); listener(target, property, val);
} }
return trap; return trap;
@ -56,71 +81,58 @@ export function stateful(target) {
return proxy; return proxy;
} }
export function isAJSReferences(arr) { export function isDLPtr(arr) {
return arr instanceof Array && ALICEJS_REFERENCES_MARKER in arr return arr instanceof Object && TARGET in arr
} }
// This lets you subscribe to a stateful object // This lets you subscribe to a stateful object
export function handle(references, callback) { export function handle(ptr, callback) {
if (!isAJSReferences(references)) const resolvedSteps = [];
throw new Error("Not an AliceJS reference set!");
if (ALICEJS_REFERENCES_MAPPING in references) { function resolve() {
const mapping = references[ALICEJS_REFERENCES_MAPPING]; let val = ptr[TARGET];
const used_props = []; for (const step of resolvedSteps) {
const used_targets = []; val = val[step];
if (typeof val !== "object") break;
}
return val;
}
const values = new Map(); // inject ourselves into nested objects
const curry = (target, i) => function subscription(tgt, prop, val) {
if (prop === resolvedSteps[i] && target === tgt) {
callback(resolve());
const pairs = []; if (typeof val === "object") {
let v = val[LISTENERS];
const partial_update = (target, prop, val) => { if (v && !v.includes(subscription)) {
if (used_props.includes(prop) && used_targets.includes(target)) { v.push(curry(val[TARGET], i + 1));
values.get(target)[prop] = val; }
}
} }
}; };
const full_update = () => {
const flattened_values = pairs.map(
(pair) => values.get(pair[0])[pair[1]],
);
const value = mapping(...flattened_values.reverse()); // imagine we have a `use(state.a[state.b])`
// simply recursively resolve any of the intermediate steps until we get to the final value
callback(value); // this will "misfire" occassionaly with a scenario like state.a[state.b][state.c] and call the listener more than needed
}; // it is up to the caller to not implode
for (let i in ptr[STEPS]) {
for (const p of references) { let step = ptr[STEPS][i];
const target = p.target; if (typeof step === "object" && step[TARGET]) {
const prop = p.property; handle(step, val => {
resolvedSteps[i] = val;
used_props.push(prop); callback(resolve());
used_targets.push(target);
pairs.push([target, prop]);
if (!values.has(target)) {
values.set(target, {});
}
partial_update(target, prop, target[prop]);
target[ALICEJS_STATEFUL_LISTENERS].push((t, p, v) => {
partial_update(t, p, v);
full_update();
}); });
continue;
} }
full_update(); resolvedSteps[i] = step;
} else {
const reference = references[references.length - 1];
const subscription = (target, prop, val) => {
if (prop === reference.property && target === reference.target) {
callback(val);
}
};
reference.target[ALICEJS_STATEFUL_LISTENERS].push(subscription);
subscription(reference.target, reference.property, reference.target[reference.property]);
} }
let sub = curry(ptr[TARGET], 0);
ptr[TARGET][LISTENERS].push(sub);
sub(ptr[TARGET], resolvedSteps[0], ptr[TARGET][resolvedSteps[0]]);
} }
export function useValue(references) { export function useValue(references) {
@ -134,17 +146,18 @@ export function h(type, props, ...children) {
let newthis = stateful(Object.create(type.prototype)); let newthis = stateful(Object.create(type.prototype));
for (const name in props) { for (const name in props) {
const references = props[name]; const ptr = props[name];
if (isAJSReferences(references) && name.startsWith("bind:")) { if (isDLPtr(ptr) && name.startsWith("bind:")) {
let reference = references[references.length - 1];
const propname = name.substring(5); const propname = name.substring(5);
if (propname == "this") { if (propname == "this") {
reference.proxy[reference.property] = newthis; // todo! support nesting
ptr[PROXY][ptr[STEPS][0]] = newthis;
} else { } else {
// component two way data binding!! (exact same behavior as svelte:bind) // component two way data binding!! (exact same behavior as svelte:bind)
let isRecursive = false; let isRecursive = false;
handle(references, value => { handle(ptr, value => {
if (isRecursive) { if (isRecursive) {
isRecursive = false; isRecursive = false;
return; return;
@ -158,7 +171,7 @@ export function h(type, props, ...children) {
return; return;
} }
isRecursive = true; isRecursive = true;
reference.proxy[reference.property] = value; ptr[PROXY][ptr[STEPS][0]] = value;
}); });
} }
delete props[name]; delete props[name];
@ -204,7 +217,7 @@ export function h(type, props, ...children) {
let thenblock = props["then"]; let thenblock = props["then"];
let elseblock = props["else"]; let elseblock = props["else"];
if (isAJSReferences(condition)) { if (isDLPtr(condition)) {
if (thenblock) elm.appendChild(thenblock); if (thenblock) elm.appendChild(thenblock);
if (elseblock) elm.appendChild(elseblock); if (elseblock) elm.appendChild(elseblock);
@ -253,7 +266,7 @@ export function h(type, props, ...children) {
const predicate = props["for"]; const predicate = props["for"];
const closure = props["do"]; const closure = props["do"];
if (isAJSReferences(predicate)) { if (isDLPtr(predicate)) {
const __elms = []; const __elms = [];
let lastpredicate = []; let lastpredicate = [];
handle(predicate, val => { handle(predicate, val => {
@ -313,21 +326,21 @@ export function h(type, props, ...children) {
}) })
for (const name in props) { for (const name in props) {
const references = props[name]; const ptr = props[name];
if (isAJSReferences(references) && name.startsWith("bind:")) { if (isDLPtr(ptr) && name.startsWith("bind:")) {
let reference = references[references.length - 1];
const propname = name.substring(5); const propname = name.substring(5);
if (propname == "this") { if (propname == "this") {
reference.proxy[reference.property] = elm; // todo! support nesting
ptr[PROXY][ptr[STEPS][0]] = elm;
} else if (propname == "value") { } else if (propname == "value") {
handle(references, value => elm.value = value); handle(ptr, value => elm.value = value);
elm.addEventListener("change", () => { elm.addEventListener("change", () => {
reference.proxy[reference.property] = elm.value; ptr[PROXY][ptr[STEPS][0]] = elm.value;
}) })
} else if (propname == "checked") { } else if (propname == "checked") {
handle(references, value => elm.checked = value); handle(ptr, value => elm.checked = value);
elm.addEventListener("click", () => { elm.addEventListener("click", () => {
reference.proxy[reference.property] = elm.checked; ptr[PROXY][ptr[STEPS][0]] = elm.checked;
}) })
} }
delete props[name]; delete props[name];
@ -340,13 +353,13 @@ export function h(type, props, ...children) {
return; return;
} }
if (isAJSReferences(classlist)) { if (isDLPtr(classlist)) {
handle(classlist, classname => elm.className = classname); handle(classlist, classname => elm.className = classname);
return; return;
} }
for (const name of classlist) { for (const name of classlist) {
if (isAJSReferences(name)) { if (isDLPtr(name)) {
let oldvalue = null; let oldvalue = null;
handle(name, value => { handle(name, value => {
if (typeof oldvalue === "string") { if (typeof oldvalue === "string") {
@ -364,7 +377,7 @@ export function h(type, props, ...children) {
// apply the non-reactive properties // apply the non-reactive properties
for (const name in props) { for (const name in props) {
const prop = props[name]; const prop = props[name];
if (isAJSReferences(prop)) { if (isDLPtr(prop)) {
handle(prop, (val) => { handle(prop, (val) => {
JSXAddAttributes(elm, name, val); JSXAddAttributes(elm, name, val);
}); });
@ -378,7 +391,7 @@ export function h(type, props, ...children) {
// glue for nested children // glue for nested children
function JSXAddChild(child, cb) { function JSXAddChild(child, cb) {
if (isAJSReferences(child)) { if (isDLPtr(child)) {
let appended = []; let appended = [];
handle(child, (val) => { handle(child, (val) => {
if (appended.length > 1) { if (appended.length > 1) {