Skip to content

Commit 836bc60

Browse files
Rich-Harrisadiguba
andauthored
feat: omit unnecessary setters from bindings (#13269)
* remove unnecessary closure * lint * reuse logic * feat: omit unnecessary setters in bindings * changeset --------- Co-authored-by: adiguba <[email protected]>
1 parent c1d8eb3 commit 836bc60

File tree

9 files changed

+175
-146
lines changed

9 files changed

+175
-146
lines changed

.changeset/stupid-dodos-hide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
feat: unwrap function expressions where possible, and optimise bindings

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

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,20 @@ export function BindDirective(node, context) {
3636
);
3737
}
3838

39-
const getter = b.thunk(/** @type {Expression} */ (context.visit(expression)));
40-
41-
const setter = b.arrow(
42-
[b.id('$$value')],
43-
/** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value'))))
39+
const get = b.thunk(/** @type {Expression} */ (context.visit(expression)));
40+
41+
/** @type {Expression | undefined} */
42+
let set = b.unthunk(
43+
b.arrow(
44+
[b.id('$$value')],
45+
/** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value'))))
46+
)
4447
);
4548

49+
if (get === set) {
50+
set = undefined;
51+
}
52+
4653
/** @type {CallExpression} */
4754
let call;
4855

@@ -52,101 +59,106 @@ export function BindDirective(node, context) {
5259
b.literal(node.name),
5360
b.literal(property.event),
5461
context.state.node,
55-
setter,
56-
property.bidirectional && getter
62+
set ?? get,
63+
property.bidirectional && get
5764
);
5865
} else {
5966
// special cases
6067
switch (node.name) {
6168
// window
6269
case 'online':
63-
call = b.call(`$.bind_online`, setter);
70+
call = b.call(`$.bind_online`, set ?? get);
6471
break;
6572

6673
case 'scrollX':
6774
case 'scrollY':
6875
call = b.call(
6976
'$.bind_window_scroll',
7077
b.literal(node.name === 'scrollX' ? 'x' : 'y'),
71-
getter,
72-
setter
78+
get,
79+
set
7380
);
7481
break;
7582

7683
case 'innerWidth':
7784
case 'innerHeight':
7885
case 'outerWidth':
7986
case 'outerHeight':
80-
call = b.call('$.bind_window_size', b.literal(node.name), setter);
87+
call = b.call('$.bind_window_size', b.literal(node.name), set ?? get);
8188
break;
8289

8390
// document
8491
case 'activeElement':
85-
call = b.call('$.bind_active_element', setter);
92+
call = b.call('$.bind_active_element', set ?? get);
8693
break;
8794

8895
// media
8996
case 'muted':
90-
call = b.call(`$.bind_muted`, context.state.node, getter, setter);
97+
call = b.call(`$.bind_muted`, context.state.node, get, set);
9198
break;
9299
case 'paused':
93-
call = b.call(`$.bind_paused`, context.state.node, getter, setter);
100+
call = b.call(`$.bind_paused`, context.state.node, get, set);
94101
break;
95102
case 'volume':
96-
call = b.call(`$.bind_volume`, context.state.node, getter, setter);
103+
call = b.call(`$.bind_volume`, context.state.node, get, set);
97104
break;
98105
case 'playbackRate':
99-
call = b.call(`$.bind_playback_rate`, context.state.node, getter, setter);
106+
call = b.call(`$.bind_playback_rate`, context.state.node, get, set);
100107
break;
101108
case 'currentTime':
102-
call = b.call(`$.bind_current_time`, context.state.node, getter, setter);
109+
call = b.call(`$.bind_current_time`, context.state.node, get, set);
103110
break;
104111
case 'buffered':
105-
call = b.call(`$.bind_buffered`, context.state.node, setter);
112+
call = b.call(`$.bind_buffered`, context.state.node, set ?? get);
106113
break;
107114
case 'played':
108-
call = b.call(`$.bind_played`, context.state.node, setter);
115+
call = b.call(`$.bind_played`, context.state.node, set ?? get);
109116
break;
110117
case 'seekable':
111-
call = b.call(`$.bind_seekable`, context.state.node, setter);
118+
call = b.call(`$.bind_seekable`, context.state.node, set ?? get);
112119
break;
113120
case 'seeking':
114-
call = b.call(`$.bind_seeking`, context.state.node, setter);
121+
call = b.call(`$.bind_seeking`, context.state.node, set ?? get);
115122
break;
116123
case 'ended':
117-
call = b.call(`$.bind_ended`, context.state.node, setter);
124+
call = b.call(`$.bind_ended`, context.state.node, set ?? get);
118125
break;
119126
case 'readyState':
120-
call = b.call(`$.bind_ready_state`, context.state.node, setter);
127+
call = b.call(`$.bind_ready_state`, context.state.node, set ?? get);
121128
break;
122129

123130
// dimensions
124131
case 'contentRect':
125132
case 'contentBoxSize':
126133
case 'borderBoxSize':
127134
case 'devicePixelContentBoxSize':
128-
call = b.call('$.bind_resize_observer', context.state.node, b.literal(node.name), setter);
135+
call = b.call(
136+
'$.bind_resize_observer',
137+
context.state.node,
138+
b.literal(node.name),
139+
set ?? get
140+
);
129141
break;
130142

131143
case 'clientWidth':
132144
case 'clientHeight':
133145
case 'offsetWidth':
134146
case 'offsetHeight':
135-
call = b.call('$.bind_element_size', context.state.node, b.literal(node.name), setter);
147+
call = b.call('$.bind_element_size', context.state.node, b.literal(node.name), set ?? get);
136148
break;
137149

138150
// various
139151
case 'value': {
140152
if (parent?.type === 'RegularElement' && parent.name === 'select') {
141-
call = b.call(`$.bind_select_value`, context.state.node, getter, setter);
153+
call = b.call(`$.bind_select_value`, context.state.node, get, set);
142154
} else {
143-
call = b.call(`$.bind_value`, context.state.node, getter, setter);
155+
call = b.call(`$.bind_value`, context.state.node, get, set);
144156
}
145157
break;
146158
}
147159

148160
case 'files':
149-
call = b.call(`$.bind_files`, context.state.node, getter, setter);
161+
call = b.call(`$.bind_files`, context.state.node, get, set);
150162
break;
151163

152164
case 'this':
@@ -160,18 +172,18 @@ export function BindDirective(node, context) {
160172
'$.bind_content_editable',
161173
b.literal(node.name),
162174
context.state.node,
163-
getter,
164-
setter
175+
get,
176+
set
165177
);
166178
break;
167179

168180
// checkbox/radio
169181
case 'checked':
170-
call = b.call(`$.bind_checked`, context.state.node, getter, setter);
182+
call = b.call(`$.bind_checked`, context.state.node, get, set);
171183
break;
172184

173185
case 'focused':
174-
call = b.call(`$.bind_focused`, context.state.node, setter);
186+
call = b.call(`$.bind_focused`, context.state.node, set ?? get);
175187
break;
176188

177189
case 'group': {
@@ -184,7 +196,7 @@ export function BindDirective(node, context) {
184196

185197
// We need to additionally invoke the value attribute signal to register it as a dependency,
186198
// so that when the value is updated, the group binding is updated
187-
let group_getter = getter;
199+
let group_getter = get;
188200

189201
if (parent?.type === 'RegularElement') {
190202
const value = /** @type {any[]} */ (
@@ -215,7 +227,7 @@ export function BindDirective(node, context) {
215227
b.array(indexes),
216228
context.state.node,
217229
group_getter,
218-
setter
230+
set ?? get
219231
);
220232
break;
221233
}

packages/svelte/src/compiler/utils/builders.js

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -419,19 +419,31 @@ export function template(elements, expressions) {
419419
* @returns {ESTree.Expression}
420420
*/
421421
export function thunk(expression, async = false) {
422+
const fn = arrow([], expression);
423+
if (async) fn.async = true;
424+
return unthunk(fn);
425+
}
426+
427+
/**
428+
* Replace "(arg) => func(arg)" to "func"
429+
* @param {ESTree.Expression} expression
430+
* @returns {ESTree.Expression}
431+
*/
432+
export function unthunk(expression) {
422433
if (
423-
expression.type === 'CallExpression' &&
424-
expression.callee.type !== 'Super' &&
425-
expression.callee.type !== 'MemberExpression' &&
426-
expression.callee.type !== 'CallExpression' &&
427-
expression.arguments.length === 0
434+
expression.type === 'ArrowFunctionExpression' &&
435+
expression.async === false &&
436+
expression.body.type === 'CallExpression' &&
437+
expression.body.callee.type === 'Identifier' &&
438+
expression.params.length === expression.body.arguments.length &&
439+
expression.params.every((param, index) => {
440+
const arg = /** @type {ESTree.SimpleCallExpression} */ (expression.body).arguments[index];
441+
return param.type === 'Identifier' && arg.type === 'Identifier' && param.name === arg.name;
442+
})
428443
) {
429-
return expression.callee;
444+
return expression.body.callee;
430445
}
431-
432-
const fn = arrow([], expression);
433-
if (async) fn.async = true;
434-
return fn;
446+
return expression;
435447
}
436448

437449
/**

packages/svelte/src/internal/client/dom/elements/bindings/input.js

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ import { hydrating } from '../../hydration.js';
88

99
/**
1010
* @param {HTMLInputElement} input
11-
* @param {() => unknown} get_value
12-
* @param {(value: unknown) => void} update
11+
* @param {() => unknown} get
12+
* @param {(value: unknown) => void} set
1313
* @returns {void}
1414
*/
15-
export function bind_value(input, get_value, update) {
15+
export function bind_value(input, get, set = get) {
1616
listen_to_event_and_reset_event(input, 'input', () => {
1717
if (DEV && input.type === 'checkbox') {
1818
// TODO should this happen in prod too?
1919
e.bind_invalid_checkbox_value();
2020
}
2121

22-
update(is_numberlike_input(input) ? to_number(input.value) : input.value);
22+
set(is_numberlike_input(input) ? to_number(input.value) : input.value);
2323
});
2424

2525
render_effect(() => {
@@ -28,12 +28,12 @@ export function bind_value(input, get_value, update) {
2828
e.bind_invalid_checkbox_value();
2929
}
3030

31-
var value = get_value();
31+
var value = get();
3232

3333
// If we are hydrating and the value has since changed, then use the update value
3434
// from the input instead.
3535
if (hydrating && input.defaultValue !== input.value) {
36-
update(input.value);
36+
set(input.value);
3737
return;
3838
}
3939

@@ -60,11 +60,11 @@ const pending = new Set();
6060
* @param {HTMLInputElement[]} inputs
6161
* @param {null | [number]} group_index
6262
* @param {HTMLInputElement} input
63-
* @param {() => unknown} get_value
64-
* @param {(value: unknown) => void} update
63+
* @param {() => unknown} get
64+
* @param {(value: unknown) => void} set
6565
* @returns {void}
6666
*/
67-
export function bind_group(inputs, group_index, input, get_value, update) {
67+
export function bind_group(inputs, group_index, input, get, set = get) {
6868
var is_checkbox = input.getAttribute('type') === 'checkbox';
6969
var binding_group = inputs;
7070

@@ -91,14 +91,14 @@ export function bind_group(inputs, group_index, input, get_value, update) {
9191
value = get_binding_group_value(binding_group, value, input.checked);
9292
}
9393

94-
update(value);
94+
set(value);
9595
},
9696
// TODO better default value handling
97-
() => update(is_checkbox ? [] : null)
97+
() => set(is_checkbox ? [] : null)
9898
);
9999

100100
render_effect(() => {
101-
var value = get_value();
101+
var value = get();
102102

103103
// If we are hydrating and the value has since changed, then use the update value
104104
// from the input instead.
@@ -147,29 +147,29 @@ export function bind_group(inputs, group_index, input, get_value, update) {
147147
value = hydration_input?.__value;
148148
}
149149

150-
update(value);
150+
set(value);
151151
}
152152
});
153153
}
154154

155155
/**
156156
* @param {HTMLInputElement} input
157-
* @param {() => unknown} get_value
158-
* @param {(value: unknown) => void} update
157+
* @param {() => unknown} get
158+
* @param {(value: unknown) => void} set
159159
* @returns {void}
160160
*/
161-
export function bind_checked(input, get_value, update) {
161+
export function bind_checked(input, get, set = get) {
162162
listen_to_event_and_reset_event(input, 'change', () => {
163163
var value = input.checked;
164-
update(value);
164+
set(value);
165165
});
166166

167-
if (get_value() == undefined) {
168-
update(false);
167+
if (get() == undefined) {
168+
set(false);
169169
}
170170

171171
render_effect(() => {
172-
var value = get_value();
172+
var value = get();
173173
input.checked = Boolean(value);
174174
});
175175
}
@@ -215,15 +215,15 @@ function to_number(value) {
215215

216216
/**
217217
* @param {HTMLInputElement} input
218-
* @param {() => FileList | null} get_value
219-
* @param {(value: FileList | null) => void} update
218+
* @param {() => FileList | null} get
219+
* @param {(value: FileList | null) => void} set
220220
*/
221-
export function bind_files(input, get_value, update) {
221+
export function bind_files(input, get, set = get) {
222222
listen_to_event_and_reset_event(input, 'change', () => {
223-
update(input.files);
223+
set(input.files);
224224
});
225225

226226
render_effect(() => {
227-
input.files = get_value();
227+
input.files = get();
228228
});
229229
}

0 commit comments

Comments
 (0)