state-onchange-roots
Rich Harris 5 months ago
parent a832f8442c
commit 31bc6a4b89

@ -8,7 +8,8 @@ import {
get_descriptor, get_descriptor,
get_prototype_of, get_prototype_of,
is_array, is_array,
object_prototype object_prototype,
run_all
} from '../shared/utils.js'; } from '../shared/utils.js';
import { PROXY_ONCHANGE_SYMBOL, STATE_SYMBOL } from './constants.js'; import { PROXY_ONCHANGE_SYMBOL, STATE_SYMBOL } from './constants.js';
import { get_stack } from './dev/tracing.js'; import { get_stack } from './dev/tracing.js';
@ -33,31 +34,25 @@ function identity(fn) {
* @returns {T} * @returns {T}
*/ */
export function proxy(value, onchange) { export function proxy(value, onchange) {
return create_proxy(value, onchange ? [onchange] : []);
}
/**
* @template T
* @param {T} value
* @param {Array<() => void>} onchanges
* @returns {T}
*/
export function create_proxy(value, onchanges) {
// if non-proxyable, or is already a proxy, return `value` // if non-proxyable, or is already a proxy, return `value`
if (typeof value !== 'object' || value === null) { if (typeof value !== 'object' || value === null) {
return value; return value;
} }
if (STATE_SYMBOL in value) { if (STATE_SYMBOL in value) {
if (onchange) {
// @ts-ignore
value[PROXY_ONCHANGE_SYMBOL](onchange);
}
return value; return value;
} }
if (onchange) {
// if there's an onchange we actually store that but override the value
// to store every other onchange that new proxies might add
var onchanges = new Set([onchange]);
onchange = () => {
for (let onchange of onchanges) {
onchange();
}
};
}
const prototype = get_prototype_of(value); const prototype = get_prototype_of(value);
if (prototype !== object_prototype && prototype !== array_prototype) { if (prototype !== object_prototype && prototype !== array_prototype) {
@ -116,7 +111,7 @@ export function proxy(value, onchange) {
} else { } else {
set( set(
s, s,
with_parent(() => proxy(descriptor.value, onchange)) with_parent(() => create_proxy(descriptor.value, onchanges))
); );
} }
@ -145,10 +140,10 @@ export function proxy(value, onchange) {
} }
} }
// when we delete a property if the source is a proxy we remove the current onchange from // when we delete a property if the source is a proxy we remove the parent `onchanges` from
// the proxy `onchanges` so that it doesn't trigger it anymore // the child `onchanges` so that it doesn't trigger it anymore
if (onchange && typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) { if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) {
s.v[PROXY_ONCHANGE_SYMBOL](onchange, true); s.v[PROXY_ONCHANGE_SYMBOL](onchanges, true);
} }
set(s, UNINITIALIZED); set(s, UNINITIALIZED);
@ -164,15 +159,16 @@ export function proxy(value, onchange) {
} }
if (prop === PROXY_ONCHANGE_SYMBOL) { if (prop === PROXY_ONCHANGE_SYMBOL) {
return (/** @type {(() => unknown)} */ value, /** @type {boolean} */ remove) => { return (/** @type {(Array<() => void>)} */ callbacks, /** @type {boolean} */ remove) => {
// we either add or remove the passed in value
// to the onchanges array or we set every source onchange
// to the passed in value (if it's undefined it will make the chain stop)
// if (onchange != null && value) {
if (remove) { if (remove) {
onchanges?.delete(value); let i = callbacks.length;
while (i--) {
const index = onchanges.lastIndexOf(callbacks[i]);
if (index === -1) throw new Error('TODO this should not happen');
onchanges.splice(index, 1);
}
} else { } else {
onchanges?.add(value); onchanges.push(...callbacks);
} }
}; };
} }
@ -183,7 +179,7 @@ export function proxy(value, onchange) {
// create a source, but only if it's an own property and not a prototype property // create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) { if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
s = with_parent(() => s = with_parent(() =>
source(proxy(exists ? target[prop] : UNINITIALIZED, onchange), stack) source(create_proxy(exists ? target[prop] : UNINITIALIZED, onchanges), stack)
); );
sources.set(prop, s); sources.set(prop, s);
} }
@ -195,11 +191,7 @@ export function proxy(value, onchange) {
v = Reflect.get(target, prop, receiver); v = Reflect.get(target, prop, receiver);
if ( if (is_proxied_array && array_methods.includes(/** @type {string} */ (prop))) {
is_proxied_array &&
onchange != null &&
array_methods.includes(/** @type {string} */ (prop))
) {
return batch_onchange(v); return batch_onchange(v);
} }
@ -242,7 +234,9 @@ export function proxy(value, onchange) {
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable)) (active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) { ) {
if (s === undefined) { if (s === undefined) {
s = with_parent(() => source(has ? proxy(target[prop], onchange) : UNINITIALIZED, stack)); s = with_parent(() =>
source(has ? create_proxy(target[prop], onchanges) : UNINITIALIZED, stack)
);
sources.set(prop, s); sources.set(prop, s);
} }
@ -269,12 +263,11 @@ export function proxy(value, onchange) {
var other_s = sources.get(i + ''); var other_s = sources.get(i + '');
if (other_s !== undefined) { if (other_s !== undefined) {
if ( if (
onchange &&
typeof other_s.v === 'object' && typeof other_s.v === 'object' &&
other_s.v !== null && other_s.v !== null &&
STATE_SYMBOL in other_s.v STATE_SYMBOL in other_s.v
) { ) {
other_s.v[PROXY_ONCHANGE_SYMBOL](onchange, true); other_s.v[PROXY_ONCHANGE_SYMBOL](onchanges, true);
} }
set(other_s, UNINITIALIZED); set(other_s, UNINITIALIZED);
} else if (i in target) { } else if (i in target) {
@ -299,21 +292,23 @@ export function proxy(value, onchange) {
} else { } else {
has = s.v !== UNINITIALIZED; has = s.v !== UNINITIALIZED;
// when we set a property if the source is a proxy we remove the current onchange from // when we set a property if the source is a proxy we remove the parent `onchanges` from
// the proxy `onchanges` so that it doesn't trigger it anymore // the child `onchanges` so that it doesn't trigger it anymore
if (onchange && typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) { if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) {
s.v[PROXY_ONCHANGE_SYMBOL](onchange, true); s.v[PROXY_ONCHANGE_SYMBOL](onchanges, true);
} }
} }
if (s !== undefined) { if (s !== undefined) {
set( set(
s, s,
with_parent(() => proxy(value, onchange)) with_parent(() => create_proxy(value, onchanges))
); );
} }
})(); })();
run_all(onchanges);
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop); var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
// Set the new value before updating any signals so that any listeners get the new value // Set the new value before updating any signals so that any listeners get the new value

@ -168,9 +168,17 @@ export function set(source, value, should_proxy = false) {
e.state_unsafe_mutation(); e.state_unsafe_mutation();
} }
let new_value = should_proxy ? proxy(value, source.o) : value; var onchange = source.o;
return internal_set(source, new_value); var new_value = should_proxy ? proxy(value, onchange) : value;
internal_set(source, new_value);
if (onchange && new_value !== value) {
onchange();
}
return new_value;
} }
/** /**
@ -183,11 +191,6 @@ export function internal_set(source, value) {
if (!source.equals(value)) { if (!source.equals(value)) {
var old_value = source.v; var old_value = source.v;
if (typeof old_value === 'object' && old_value != null && source.o) {
// @ts-ignore
old_value[PROXY_ONCHANGE_SYMBOL]?.(source.o, true);
}
if (is_destroying_effect) { if (is_destroying_effect) {
old_values.set(source, value); old_values.set(source, value);
} else { } else {

Loading…
Cancel
Save