Skip to content

Commit f71fe24

Browse files
committed
improve implementation to lazyily make proxy
better naming fixup simplify add comments
1 parent fd83259 commit f71fe24

File tree

4 files changed

+139
-81
lines changed

4 files changed

+139
-81
lines changed

packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,15 +278,15 @@ export const javascript_visitors_runes = {
278278

279279
if (rune === '$derived') {
280280
if (declarator.id.type === 'Identifier') {
281-
declarations.push(b.declarator(declarator.id, b.call('$.derived_proxy', b.thunk(value))));
281+
declarations.push(b.declarator(declarator.id, b.call('$.derived', b.thunk(value))));
282282
} else {
283283
const bindings = state.scope.get_bindings(declarator);
284284
const id = state.scope.generate('derived_value');
285285
declarations.push(
286286
b.declarator(
287287
b.id(id),
288288
b.call(
289-
'$.derived_proxy',
289+
'$.derived',
290290
b.thunk(
291291
b.block([
292292
b.let(declarator.id, value),

packages/svelte/src/internal/client/runtime.js

Lines changed: 130 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ let current_queued_pre_and_render_effects = [];
5959
/** @type {import('./types.js').EffectSignal[]} */
6060
let current_queued_effects = [];
6161

62-
/** @type {{t: import('./types.js').ComputationSignal, p: Array<string | symbol>, r: import('./types.js').ComputationSignal } | null} */
63-
let current_path = null;
62+
/** @type {{ s: import('./types.js').ComputationSignal, p: Array<string | symbol> } | null} */
63+
let current_derived_proxy_property = null;
6464

6565
/** @type {Array<() => void>} */
6666
let current_queued_tasks = [];
@@ -341,7 +341,7 @@ function execute_signal_fn(signal) {
341341
const previous_skip_consumer = current_skip_consumer;
342342
const is_render_effect = (flags & RENDER_EFFECT) !== 0;
343343
const previous_untracking = current_untracking;
344-
const previous_path = current_path;
344+
const previous_derived_proxy_property = current_derived_proxy_property;
345345
current_dependencies = /** @type {null | import('./types.js').Signal[]} */ (null);
346346
current_dependencies_index = 0;
347347
current_untracked_writes = null;
@@ -350,7 +350,7 @@ function execute_signal_fn(signal) {
350350
current_component_context = signal.x;
351351
current_skip_consumer = !is_flushing_effect && (flags & UNOWNED) !== 0;
352352
current_untracking = false;
353-
current_path = null;
353+
current_derived_proxy_property = null;
354354

355355
// Render effects are invoked when the UI is about to be updated - run beforeUpdate at that point
356356
if (is_render_effect && current_component_context?.u != null) {
@@ -371,8 +371,8 @@ function execute_signal_fn(signal) {
371371
} else {
372372
res = /** @type {() => V} */ (init)();
373373
}
374-
if (current_path !== null) {
375-
push_derived_path();
374+
if (current_derived_proxy_property !== null) {
375+
capture_fine_grain_derived_property();
376376
}
377377
let dependencies = /** @type {import('./types.js').Signal<unknown>[]} **/ (signal.d);
378378
if (current_dependencies !== null) {
@@ -444,7 +444,7 @@ function execute_signal_fn(signal) {
444444
current_component_context = previous_component_context;
445445
current_skip_consumer = previous_skip_consumer;
446446
current_untracking = previous_untracking;
447-
current_path = previous_path;
447+
current_derived_proxy_property = previous_derived_proxy_property;
448448
}
449449
}
450450

@@ -536,7 +536,7 @@ export function execute_effect(signal) {
536536
if ((signal.f & DESTROYED) !== 0) {
537537
return;
538538
}
539-
const teardown = signal.v;
539+
const teardown = /** @type {null | (() => void)} */ (signal.v);
540540
const previous_effect = current_effect;
541541
current_effect = signal;
542542

@@ -830,16 +830,30 @@ export async function tick() {
830830
* @returns {void}
831831
*/
832832
function update_derived(signal, force_schedule) {
833+
let derived_value =
834+
/** @type {import('./types.js').DerivedSignalValue<V> | typeof UNINITIALIZED} */ (signal.v);
835+
if (derived_value === UNINITIALIZED) {
836+
signal.v = derived_value = /** @type {import('./types.js').DerivedSignalValue<V>} */ ({
837+
p: null,
838+
v: UNINITIALIZED,
839+
o: null
840+
});
841+
}
833842
const previous_updating_derived = updating_derived;
834843
updating_derived = true;
844+
if (derived_value.p !== null) {
845+
derived_value.p = null;
846+
derived_value.o?.clear();
847+
}
835848
destroy_references(signal);
836849
const value = execute_signal_fn(signal);
837850
updating_derived = previous_updating_derived;
838851
const status = current_skip_consumer || (signal.f & UNOWNED) !== 0 ? DIRTY : CLEAN;
839852
set_signal_status(signal, status);
840853
const equals = /** @type {import('./types.js').EqualsFunctions} */ (signal.e);
841-
if (!equals(value, signal.v)) {
842-
signal.v = value;
854+
855+
if (!equals(value, derived_value.v)) {
856+
derived_value.v = value;
843857
mark_signal_consumers(signal, DIRTY, force_schedule);
844858

845859
// @ts-expect-error
@@ -939,36 +953,49 @@ export function unsubscribe_on_destroy(stores) {
939953
});
940954
}
941955

942-
function push_derived_path() {
943-
const path =
944-
/** @type {{t: import('./types.js').ComputationSignal, p: Array<string | symbol>, r: import('./types.js').ComputationSignal }} */ (
945-
current_path
956+
/**
957+
* If the `current_derived_proxy_property` is not `null` then that means we should look at the current
958+
* property on the object and see if actually need original derived object dependency. For example,
959+
* if you had this:
960+
*
961+
* a.b.c
962+
*
963+
* Under-the-hood, `a` might be a derived signal, so we'd call get() on it. Resulting in `a` being a dependency
964+
* for the currently active effect. The accessors to `b` and `c` would result in the `current_derived_proxy_property`
965+
* changing to include ['b', 'c'] in the `current_derived_proxy_property.p` paths property. We can then use that to
966+
* determine a new derived temporary signal that encapsulates a.b.c. This temporarly signal then becomes the dependency
967+
* and we no longer need to the original depdency to `a` for the current effect. Thus making
968+
*/
969+
function capture_fine_grain_derived_property() {
970+
const derived_property =
971+
/** @type {{ s: import('./types.js').ComputationSignal, p: Array<string | symbol> }} */ (
972+
current_derived_proxy_property
946973
);
947-
if (is_last_current_dependency(path.r)) {
974+
if (is_last_current_dependency(derived_property.s)) {
948975
if (current_dependencies === null) {
949976
current_dependencies_index--;
950977
} else {
951978
current_dependencies.pop();
952979
}
953980
}
954981
const derived_prop = derived(() => {
955-
let value = /** @type {any} */ (get(path.t));
956-
const property_path = path.p;
982+
let value = /** @type {any} */ (get(derived_property.s, true));
983+
const property_path = derived_property.p;
957984
for (let i = 0; i < property_path.length; i++) {
958985
value = value?.[property_path[i]];
959986
}
960987
return value;
961988
});
962-
current_path = null;
963-
get(derived_prop);
989+
current_derived_proxy_property = null;
990+
get(derived_prop, true);
964991
}
965992

966993
/**
967994
* @template V
968995
* @param {import('./types.js').Signal<V>} signal
969996
* @returns {V}
970997
*/
971-
export function get(signal) {
998+
export function get(signal, skip_derived_proxy = false) {
972999
// @ts-expect-error
9731000
if (DEV && signal.inspect && inspect_fn) {
9741001
/** @type {import('./types.js').SignalDebug} */ (signal).inspect.add(inspect_fn);
@@ -977,12 +1004,16 @@ export function get(signal) {
9771004
}
9781005

9791006
const flags = signal.f;
1007+
const is_derived = (flags & DERIVED) !== 0;
1008+
let value = signal.v;
9801009
if ((flags & DESTROYED) !== 0) {
981-
return signal.v;
1010+
return /** @type {V} */ (
1011+
is_derived ? /** @type {import('./types.js').DerivedSignalValue<V>} */ (value).v : value
1012+
);
9821013
}
9831014

984-
if (current_path !== null) {
985-
push_derived_path();
1015+
if (current_derived_proxy_property !== null) {
1016+
capture_fine_grain_derived_property();
9861017
}
9871018

9881019
if (is_signals_recorded) {
@@ -1022,18 +1053,47 @@ export function get(signal) {
10221053
}
10231054
}
10241055

1025-
if ((flags & DERIVED) !== 0 && is_signal_dirty(signal)) {
1026-
if (DEV) {
1027-
// we want to avoid tracking indirect dependencies
1028-
const previous_inspect_fn = inspect_fn;
1029-
inspect_fn = null;
1030-
update_derived(/** @type {import('./types.js').ComputationSignal<V>} **/ (signal), false);
1031-
inspect_fn = previous_inspect_fn;
1032-
} else {
1033-
update_derived(/** @type {import('./types.js').ComputationSignal<V>} **/ (signal), false);
1056+
if (is_derived) {
1057+
if (is_signal_dirty(signal)) {
1058+
if (DEV) {
1059+
// we want to avoid tracking indirect dependencies
1060+
const previous_inspect_fn = inspect_fn;
1061+
inspect_fn = null;
1062+
update_derived(/** @type {import('./types.js').ComputationSignal<V>} **/ (signal), false);
1063+
inspect_fn = previous_inspect_fn;
1064+
} else {
1065+
update_derived(/** @type {import('./types.js').ComputationSignal<V>} **/ (signal), false);
1066+
}
1067+
}
1068+
const derived_signal_value = /** @type {import('./types.js').DerivedSignalValue<V>} */ (
1069+
signal.v
1070+
);
1071+
const value = derived_signal_value.v;
1072+
// If we are working with a derived that might be an object or array, then we might also want to
1073+
// apply the fine-grain derived property heuristic to them. However, we only need this heuristic in some cases:
1074+
// - inside a user effect ($effect or $effect.pre)
1075+
// - inside another derived ($derived)
1076+
// Else we don't need to bother doing this as render effects and the rest of the internal architecture applys
1077+
// diffing which is more optimal than creating many derived signals. However, we can't do diffing inside user
1078+
// effects (far too many complications with cleanup functions etc).
1079+
if (
1080+
!skip_derived_proxy &&
1081+
is_runes(signal.x) &&
1082+
effect_active_and_not_render_effect() &&
1083+
should_proxy_derived_value(value)
1084+
) {
1085+
let proxy = derived_signal_value.p;
1086+
if (proxy === null) {
1087+
proxy = derived_signal_value.p = create_derived_proxy(
1088+
/** @type {import('./types.js').ComputationSignal<V>} **/ (signal),
1089+
value
1090+
);
1091+
}
1092+
return proxy;
10341093
}
1094+
return value;
10351095
}
1036-
return signal.v;
1096+
return /** @type {V} */ (signal.v);
10371097
}
10381098

10391099
/**
@@ -1366,30 +1426,36 @@ function is_last_current_dependency(signal) {
13661426

13671427
/**
13681428
* @template V
1369-
* @param {() => any} init
1370-
* @returns {import('./types.js').ComputationSignal<V>}
1429+
* @param {import("./types.js").ComputationSignal<V>} signal
1430+
* @param {V} value
1431+
* @param {ProxyHandler<any>} handler
1432+
* @param {(string | symbol)[]} path
1433+
* @returns {V}
13711434
*/
1372-
/*#__NO_SIDE_EFFECTS__*/
1373-
export function derived_proxy(init) {
1374-
const derived_object = derived(init);
1375-
const proxied_objects = new Map();
1376-
1377-
/**
1378-
* @param {V} value
1379-
* @param {(string | symbol)[]} path
1380-
* @returns {V}
1381-
*/
1382-
function proxify_object(value, path) {
1383-
const keys = new Set(Reflect.ownKeys(/** @type {object} */ (value)));
1384-
const proxy = new Proxy(value, handler);
1385-
proxied_objects.set(value, {
1386-
x: proxy,
1387-
k: keys,
1388-
p: path
1389-
});
1390-
return proxy;
1391-
}
1435+
function proxify_object(signal, value, handler, path) {
1436+
const keys = new Set(Reflect.ownKeys(/** @type {object} */ (value)));
1437+
const proxy = new Proxy(value, handler);
1438+
const derived_value = /** @type {import('./types.js').DerivedSignalValue<V>} */ (signal.v);
1439+
let proxied_objects = derived_value.o;
1440+
if (proxied_objects === null) {
1441+
derived_value.o = proxied_objects = new Map();
1442+
}
1443+
proxied_objects.set(value, {
1444+
x: proxy,
1445+
k: keys,
1446+
p: path
1447+
});
1448+
return proxy;
1449+
}
13921450

1451+
/**
1452+
* @template V
1453+
* @param {import("./types.js").ComputationSignal<V>} signal
1454+
* @param {V} derived_value
1455+
* @returns {V}
1456+
*/
1457+
/*#__NO_SIDE_EFFECTS__*/
1458+
function create_derived_proxy(signal, derived_value) {
13931459
const handler = {
13941460
/**
13951461
* @param {any} target
@@ -1398,12 +1464,14 @@ export function derived_proxy(init) {
13981464
*/
13991465
get(target, prop, receiver) {
14001466
const value = Reflect.get(target, prop, receiver);
1467+
const derived_value = /** @type {import('./types.js').DerivedSignalValue<V>} */ (signal.v);
1468+
const proxied_objects = /** @type {any} */ (derived_value.o);
14011469
const { k: keys, p: path } = proxied_objects.get(target);
14021470

14031471
if (
1404-
(effect_active_and_not_render_effect() || updating_derived) &&
1472+
effect_active_and_not_render_effect() &&
14051473
keys.has(prop) &&
1406-
is_last_current_dependency(proxied_derived)
1474+
is_last_current_dependency(signal)
14071475
) {
14081476
const type = typeof value;
14091477
let new_path;
@@ -1420,10 +1488,10 @@ export function derived_proxy(init) {
14201488
STATE_SYMBOL in value
14211489
) {
14221490
new_path = [...path, prop];
1423-
if (current_path !== null) {
1424-
push_derived_path();
1491+
if (current_derived_proxy_property !== null) {
1492+
capture_fine_grain_derived_property();
14251493
} else {
1426-
current_path = { t: derived_object, p: new_path, r: proxied_derived };
1494+
current_derived_proxy_property = { s: signal, p: new_path };
14271495
}
14281496
}
14291497
if (should_proxy_derived_value(value)) {
@@ -1434,29 +1502,14 @@ export function derived_proxy(init) {
14341502
if (!new_path) {
14351503
new_path = [...path, prop];
14361504
}
1437-
return proxify_object(value, new_path);
1505+
return proxify_object(signal, value, handler, new_path);
14381506
}
14391507
}
14401508
return value;
14411509
}
14421510
};
14431511

1444-
const proxied_derived = derived(() => {
1445-
const value = get(derived_object);
1446-
if (should_proxy_derived_value(value)) {
1447-
return proxify_object(value, []);
1448-
} else if (proxied_objects.size > 0) {
1449-
proxied_objects.clear();
1450-
}
1451-
return value;
1452-
});
1453-
1454-
// Cleanup when the derived is destroyed
1455-
proxied_derived.y = () => {
1456-
proxied_objects.clear();
1457-
};
1458-
1459-
return proxied_derived;
1512+
return proxify_object(signal, derived_value, handler, []);
14601513
}
14611514

14621515
/**

packages/svelte/src/internal/client/types.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ export type SourceSignalDebug = {
8484
inspect: Set<Function>;
8585
};
8686

87+
export type DerivedSignalValue<V> = {
88+
v: V;
89+
p: null | V;
90+
o: null | Map<any, { x: any; k: Set<string | symbol>; p: Array<string | symbol> }>;
91+
};
92+
8793
export type ComputationSignal<V = unknown> = {
8894
/** block: The block associated with this effect/computed */
8995
b: null | Block;
@@ -108,7 +114,7 @@ export type ComputationSignal<V = unknown> = {
108114
/** references: Anything that a signal owns */
109115
r: null | ComputationSignal[];
110116
/** value: The latest value for this signal, doubles as the teardown for effects */
111-
v: V;
117+
v: V | DerivedSignalValue<V>;
112118
/** level: the depth from the root signal, used for ordering render/pre-effects topologically **/
113119
l: number;
114120
};

packages/svelte/src/internal/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export {
77
source,
88
mutable_source,
99
derived,
10-
derived_proxy,
1110
derived_safe_equal,
1211
prop,
1312
user_effect,

0 commit comments

Comments
 (0)