mirror of
https://github.com/MercuryWorkshop/dreamlandjs.git
synced 2025-05-16 15:40:01 -04:00
completely rewrite reactivity
This commit is contained in:
parent
444d7d7f58
commit
2d577a6a99
2 changed files with 211 additions and 190 deletions
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
219
js.js
219
js.js
|
@ -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;
|
||||||
const values = new Map();
|
|
||||||
|
|
||||||
const pairs = [];
|
|
||||||
|
|
||||||
const partial_update = (target, prop, val) => {
|
|
||||||
if (used_props.includes(prop) && used_targets.includes(target)) {
|
|
||||||
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());
|
|
||||||
|
|
||||||
callback(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const p of references) {
|
|
||||||
const target = p.target;
|
|
||||||
const prop = p.property;
|
|
||||||
|
|
||||||
used_props.push(prop);
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
full_update();
|
return val;
|
||||||
} 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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inject ourselves into nested objects
|
||||||
|
const curry = (target, i) => function subscription(tgt, prop, val) {
|
||||||
|
if (prop === resolvedSteps[i] && target === tgt) {
|
||||||
|
callback(resolve());
|
||||||
|
|
||||||
|
if (typeof val === "object") {
|
||||||
|
let v = val[LISTENERS];
|
||||||
|
if (v && !v.includes(subscription)) {
|
||||||
|
v.push(curry(val[TARGET], i + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// imagine we have a `use(state.a[state.b])`
|
||||||
|
// simply recursively resolve any of the intermediate steps until we get to the final 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]) {
|
||||||
|
let step = ptr[STEPS][i];
|
||||||
|
if (typeof step === "object" && step[TARGET]) {
|
||||||
|
handle(step, val => {
|
||||||
|
resolvedSteps[i] = val;
|
||||||
|
callback(resolve());
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
resolvedSteps[i] = step;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue