Skip to content

Commit 714c042

Browse files
extract onchange callbacks from options (#15579)
* WIP * extract onchange callbacks * const * tweak * docs * fix: unwrap args in case of spread * fix: revert unwrap args in case of spread --------- Co-authored-by: paoloricciuti <[email protected]>
1 parent a33ff30 commit 714c042

File tree

7 files changed

+82
-83
lines changed

7 files changed

+82
-83
lines changed

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as b from '../../../../utils/builders.js';
66
import { regex_invalid_identifier_chars } from '../../../patterns.js';
77
import { get_rune } from '../../../scope.js';
88
import { should_proxy } from '../utils.js';
9+
import { get_onchange } from './shared/state.js';
910

1011
/**
1112
* @param {ClassBody} node
@@ -117,13 +118,16 @@ export function ClassBody(node, context) {
117118
);
118119

119120
if (field.kind === 'state' || field.kind === 'raw_state') {
120-
let arg = definition.value.arguments[1];
121-
let options = arg && /** @type {Expression} **/ (context.visit(arg, child_state));
121+
const onchange = get_onchange(
122+
/** @type {Expression} */ (definition.value.arguments[1]),
123+
// @ts-ignore mismatch between Context and ComponentContext. TODO look into
124+
context
125+
);
122126

123127
value =
124128
field.kind === 'state' && should_proxy(init, context.state.scope)
125-
? b.call('$.assignable_proxy', init, options)
126-
: b.call('$.state', init, options);
129+
? b.call('$.assignable_proxy', init, onchange)
130+
: b.call('$.state', init, onchange);
127131
} else {
128132
value = b.call('$.derived', field.kind === 'derived_by' ? init : b.thunk(init));
129133
}

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as assert from '../../../../utils/assert.js';
88
import { get_rune } from '../../../scope.js';
99
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';
1010
import { is_hoisted_function } from '../../utils.js';
11+
import { get_onchange } from './shared/state.js';
1112

1213
/**
1314
* @param {VariableDeclaration} node
@@ -117,34 +118,34 @@ export function VariableDeclaration(node, context) {
117118

118119
const args = /** @type {CallExpression} */ (init).arguments;
119120
const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0;
120-
let options =
121-
args.length === 2 ? /** @type {Expression} */ (context.visit(args[1])) : undefined;
121+
122+
const onchange = get_onchange(/** @type {Expression} */ (args[1]), context);
122123

123124
if (rune === '$state' || rune === '$state.raw') {
124125
/**
125126
* @param {Identifier} id
126127
* @param {Expression} value
127-
* @param {Expression} [options]
128+
* @param {Expression} [onchange]
128129
*/
129-
const create_state_declarator = (id, value, options) => {
130+
const create_state_declarator = (id, value, onchange) => {
130131
const binding = /** @type {Binding} */ (context.state.scope.get(id.name));
131132
const proxied = rune === '$state' && should_proxy(value, context.state.scope);
132133
const is_state = is_state_source(binding, context.state.analysis);
133134

134135
if (proxied) {
135-
return b.call(is_state ? '$.assignable_proxy' : '$.proxy', value, options);
136+
return b.call(is_state ? '$.assignable_proxy' : '$.proxy', value, onchange);
136137
}
137138

138139
if (is_state) {
139-
return b.call('$.state', value, options);
140+
return b.call('$.state', value, onchange);
140141
}
141142

142143
return value;
143144
};
144145

145146
if (declarator.id.type === 'Identifier') {
146147
declarations.push(
147-
b.declarator(declarator.id, create_state_declarator(declarator.id, value, options))
148+
b.declarator(declarator.id, create_state_declarator(declarator.id, value, onchange))
148149
);
149150
} else {
150151
const tmp = context.state.scope.generate('tmp');
@@ -157,7 +158,7 @@ export function VariableDeclaration(node, context) {
157158
return b.declarator(
158159
path.node,
159160
binding?.kind === 'state' || binding?.kind === 'raw_state'
160-
? create_state_declarator(binding.node, value, options)
161+
? create_state_declarator(binding.node, value, onchange)
161162
: value
162163
);
163164
})
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/** @import { Expression, Property } from 'estree' */
2+
/** @import { ComponentContext } from '../../types' */
3+
import * as b from '../../../../../utils/builders.js';
4+
5+
/**
6+
* Extract the `onchange` callback from the options passed to `$state`
7+
* @param {Expression} options
8+
* @param {ComponentContext} context
9+
* @returns {Expression | undefined}
10+
*/
11+
export function get_onchange(options, context) {
12+
if (!options) return;
13+
14+
if (options.type === 'ObjectExpression') {
15+
const onchange = /** @type {Property | undefined} */ (
16+
options.properties.find(
17+
(property) =>
18+
property.type === 'Property' &&
19+
!property.computed &&
20+
property.key.type === 'Identifier' &&
21+
property.key.name === 'onchange'
22+
)
23+
);
24+
25+
if (!onchange) return;
26+
27+
return /** @type {Expression} */ (context.visit(onchange.value));
28+
}
29+
30+
return b.member(/** @type {Expression} */ (context.visit(options)), 'onchange');
31+
}

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,7 @@ export {
113113
user_effect,
114114
user_pre_effect
115115
} from './reactivity/effects.js';
116-
export {
117-
mutable_source,
118-
mutate,
119-
set,
120-
state,
121-
update,
122-
update_pre,
123-
get_options
124-
} from './reactivity/sources.js';
116+
export { mutable_source, mutate, set, state, update, update_pre } from './reactivity/sources.js';
125117
export {
126118
prop,
127119
rest_props,

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

Lines changed: 26 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -28,47 +28,33 @@ function identity(fn) {
2828
return fn;
2929
}
3030

31-
/**
32-
* @param {ValueOptions | undefined} options
33-
* @returns {ValueOptions | undefined}
34-
*/
35-
function clone_options(options) {
36-
return options != null
37-
? {
38-
onchange: options.onchange
39-
}
40-
: undefined;
41-
}
42-
4331
/** @type {ProxyMetadata | null} */
4432
var parent_metadata = null;
4533

4634
/**
4735
* @template T
4836
* @param {T} value
49-
* @param {ValueOptions} [_options]
37+
* @param {() => void} [onchange]
5038
* @param {Source<T>} [prev] dev mode only
5139
* @returns {T}
5240
*/
53-
export function proxy(value, _options, prev) {
41+
export function proxy(value, onchange, prev) {
5442
// if non-proxyable, or is already a proxy, return `value`
5543
if (typeof value !== 'object' || value === null) {
5644
return value;
5745
}
5846

59-
var options = clone_options(_options);
60-
6147
if (STATE_SYMBOL in value) {
6248
// @ts-ignore
63-
value[PROXY_ONCHANGE_SYMBOL](options?.onchange);
49+
value[PROXY_ONCHANGE_SYMBOL](onchange);
6450
return value;
6551
}
6652

67-
if (options?.onchange) {
53+
if (onchange) {
6854
// if there's an onchange we actually store that but override the value
6955
// to store every other onchange that new proxies might add
70-
var onchanges = new Set([options.onchange]);
71-
options.onchange = () => {
56+
var onchanges = new Set([onchange]);
57+
onchange = () => {
7258
for (let onchange of onchanges) {
7359
onchange();
7460
}
@@ -116,10 +102,7 @@ export function proxy(value, _options, prev) {
116102
if (is_proxied_array) {
117103
// We need to create the length source eagerly to ensure that
118104
// mutations to the array are properly synced with our proxy
119-
sources.set(
120-
'length',
121-
source(/** @type {any[]} */ (value).length, clone_options(options), stack)
122-
);
105+
sources.set('length', source(/** @type {any[]} */ (value).length, onchange, stack));
123106
}
124107

125108
/** @type {ProxyMetadata} */
@@ -165,12 +148,12 @@ export function proxy(value, _options, prev) {
165148
var s = sources.get(prop);
166149

167150
if (s === undefined) {
168-
s = with_parent(() => source(descriptor.value, clone_options(options), stack));
151+
s = with_parent(() => source(descriptor.value, onchange, stack));
169152
sources.set(prop, s);
170153
} else {
171154
set(
172155
s,
173-
with_parent(() => proxy(descriptor.value, options))
156+
with_parent(() => proxy(descriptor.value, onchange))
174157
);
175158
}
176159

@@ -184,7 +167,7 @@ export function proxy(value, _options, prev) {
184167
if (prop in target) {
185168
sources.set(
186169
prop,
187-
with_parent(() => source(UNINITIALIZED, clone_options(options), stack))
170+
with_parent(() => source(UNINITIALIZED, onchange, stack))
188171
);
189172
}
190173
} else {
@@ -201,7 +184,7 @@ export function proxy(value, _options, prev) {
201184
// when we delete a property if the source is a proxy we remove the current onchange from
202185
// the proxy `onchanges` so that it doesn't trigger it anymore
203186
if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) {
204-
s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true);
187+
s.v[PROXY_ONCHANGE_SYMBOL](onchange, true);
205188
}
206189
set(s, UNINITIALIZED);
207190
update_version(version);
@@ -227,18 +210,14 @@ export function proxy(value, _options, prev) {
227210
// we either add or remove the passed in value
228211
// to the onchanges array or we set every source onchange
229212
// to the passed in value (if it's undefined it will make the chain stop)
230-
if (options?.onchange != null && value && !remove) {
213+
if (onchange != null && value && !remove) {
231214
onchanges?.add?.(value);
232-
} else if (options?.onchange != null && value) {
215+
} else if (onchange != null && value) {
233216
onchanges?.delete?.(value);
234217
} else {
235-
options = {
236-
onchange: value
237-
};
218+
onchange = value;
238219
for (let [, s] of sources) {
239-
if (s.o) {
240-
s.o.onchange = value;
241-
}
220+
s.o = value;
242221
}
243222
}
244223
};
@@ -249,7 +228,7 @@ export function proxy(value, _options, prev) {
249228

250229
// create a source, but only if it's an own property and not a prototype property
251230
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
252-
let opt = clone_options(options);
231+
let opt = onchange;
253232
s = with_parent(() =>
254233
source(proxy(exists ? target[prop] : UNINITIALIZED, opt), opt, stack)
255234
);
@@ -281,7 +260,7 @@ export function proxy(value, _options, prev) {
281260

282261
if (
283262
is_proxied_array &&
284-
options?.onchange != null &&
263+
onchange != null &&
285264
array_methods.includes(/** @type {string} */ (prop))
286265
) {
287266
return batch_onchange(v);
@@ -330,7 +309,7 @@ export function proxy(value, _options, prev) {
330309
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
331310
) {
332311
if (s === undefined) {
333-
let opt = clone_options(options);
312+
let opt = onchange;
334313
s = with_parent(() => source(has ? proxy(target[prop], opt) : UNINITIALIZED, opt, stack));
335314
sources.set(prop, s);
336315
}
@@ -362,14 +341,14 @@ export function proxy(value, _options, prev) {
362341
other_s.v !== null &&
363342
STATE_SYMBOL in other_s.v
364343
) {
365-
other_s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true);
344+
other_s.v[PROXY_ONCHANGE_SYMBOL](onchange, true);
366345
}
367346
set(other_s, UNINITIALIZED);
368347
} else if (i in target) {
369348
// If the item exists in the original, we need to create a uninitialized source,
370349
// else a later read of the property would result in a source being created with
371350
// the value of the original item at that index.
372-
other_s = with_parent(() => source(UNINITIALIZED, clone_options(options), stack));
351+
other_s = with_parent(() => source(UNINITIALIZED, onchange, stack));
373352
sources.set(i + '', other_s);
374353
}
375354
}
@@ -381,7 +360,7 @@ export function proxy(value, _options, prev) {
381360
// object property before writing to that property.
382361
if (s === undefined) {
383362
if (!has || get_descriptor(target, prop)?.writable) {
384-
const opt = clone_options(options);
363+
const opt = onchange;
385364
s = with_parent(() => source(undefined, opt, stack));
386365
set(
387366
s,
@@ -394,11 +373,11 @@ export function proxy(value, _options, prev) {
394373
// when we set a property if the source is a proxy we remove the current onchange from
395374
// the proxy `onchanges` so that it doesn't trigger it anymore
396375
if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) {
397-
s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true);
376+
s.v[PROXY_ONCHANGE_SYMBOL](onchange, true);
398377
}
399378
set(
400379
s,
401-
with_parent(() => proxy(value, clone_options(options)))
380+
with_parent(() => proxy(value, onchange))
402381
);
403382
}
404383
})();
@@ -464,11 +443,11 @@ export function proxy(value, _options, prev) {
464443
/**
465444
* @template T
466445
* @param {T} value
467-
* @param {ValueOptions} [options]
446+
* @param {() => void} [onchange]
468447
* @returns {Source<T>}
469448
*/
470-
export function assignable_proxy(value, options) {
471-
return state(proxy(value, options), options);
449+
export function assignable_proxy(value, onchange) {
450+
return state(proxy(value, onchange), onchange);
472451
}
473452

474453
/**

packages/svelte/src/internal/client/reactivity/sources.js

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export function batch_onchange(fn) {
7777
/**
7878
* @template V
7979
* @param {V} v
80-
* @param {ValueOptions} [o]
80+
* @param {() => void} [o]
8181
* @param {Error | null} [stack]
8282
* @returns {Source<V>}
8383
*/
@@ -102,18 +102,10 @@ export function source(v, o, stack) {
102102
return signal;
103103
}
104104

105-
/**
106-
* @param {Source} source
107-
* @returns {ValueOptions | undefined}
108-
*/
109-
export function get_options(source) {
110-
return source.o;
111-
}
112-
113105
/**
114106
* @template V
115107
* @param {V} v
116-
* @param {ValueOptions} [o]
108+
* @param {() => void} [o]
117109
* @param {Error | null} [stack]
118110
*/
119111
export function state(v, o, stack) {
@@ -196,11 +188,11 @@ export function internal_set(source, value) {
196188
if (!source.equals(value)) {
197189
var old_value = source.v;
198190

199-
if (typeof old_value === 'object' && old_value != null && source.o?.onchange) {
191+
if (typeof old_value === 'object' && old_value != null && source.o) {
200192
// @ts-ignore
201193
const remove = old_value[PROXY_ONCHANGE_SYMBOL];
202194
if (remove && typeof remove === 'function') {
203-
remove(source.o?.onchange, true);
195+
remove(source.o, true);
204196
}
205197
}
206198

@@ -257,7 +249,7 @@ export function internal_set(source, value) {
257249
inspect_effects.clear();
258250
}
259251

260-
var onchange = source.o?.onchange;
252+
var onchange = source.o;
261253
if (onchange) {
262254
if (onchange_batch) {
263255
onchange_batch.add(onchange);

0 commit comments

Comments
 (0)