-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: #4265 prevent double update when bind to object #8992
Changes from all commits
bfeda78
1cc1280
0341e0b
841b641
0e5e471
34dcc5d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,7 +38,8 @@ export function invalidate(renderer, scope, node, names, main_execution_context | |
* @param {import('estree').Expression} [node] | ||
*/ | ||
function get_invalidated(variable, node) { | ||
if (main_execution_context && !variable.subscribable && variable.name[0] !== '$') { | ||
const is_props = !!variable.export_name; | ||
if (main_execution_context && !is_props && !variable.subscribable && variable.name[0] !== '$') { | ||
return node; | ||
} | ||
return renderer_invalidate(renderer, variable.name, undefined, main_execution_context); | ||
|
@@ -61,8 +62,9 @@ export function invalidate(renderer, scope, node, names, main_execution_context | |
return x`@set_store_value(${head.name.slice(1)}, ${node}, ${head.name}, ${extra_args})`; | ||
} | ||
|
||
const is_props = !!head.export_name; | ||
let invalidate; | ||
if (!main_execution_context) { | ||
if (!main_execution_context || is_props) { | ||
const pass_value = | ||
extra_args.length > 0 || | ||
(node.type === 'AssignmentExpression' && node.left.type !== 'Identifier') || | ||
|
@@ -96,8 +98,9 @@ export function invalidate(renderer, scope, node, names, main_execution_context | |
*/ | ||
export function renderer_invalidate(renderer, name, value, main_execution_context = false) { | ||
const variable = renderer.component.var_lookup.get(name); | ||
const is_props = variable && variable.export_name && !variable.module; | ||
if (variable && variable.subscribable && (variable.reassigned || variable.export_name)) { | ||
if (main_execution_context) { | ||
if (main_execution_context && !is_props) { | ||
return x`${`$$subscribe_${name}`}(${value || name})`; | ||
} else { | ||
const member = renderer.context_lookup.get(name); | ||
|
@@ -124,6 +127,9 @@ export function renderer_invalidate(renderer, name, value, main_execution_contex | |
const member = renderer.context_lookup.get(name); | ||
return x`$$invalidate(${member.index}, ${value})`; | ||
} | ||
} else if (main_execution_context && is_props) { | ||
const member = renderer.context_lookup.get(name); | ||
return x`$$invalidate(${member.index}, ${name})`; | ||
Comment on lines
+130
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Compiler also inserts |
||
} | ||
if (main_execution_context) return; | ||
// if this is a reactive declaration, invalidate dependencies recursively | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -345,8 +345,8 @@ export default class InlineComponentWrapper extends Wrapper { | |
block.maintain_context = true; // TODO put this somewhere more logical | ||
} | ||
block.chunks.init.push(b` | ||
function ${id}(#value) { | ||
${callee}(${args}); | ||
function ${id}(#value, #skip_binding) { | ||
${callee}(#skip_binding, ${args}); | ||
} | ||
`); | ||
let invalidate_binding = b` | ||
|
@@ -360,9 +360,15 @@ export default class InlineComponentWrapper extends Wrapper { | |
} | ||
`; | ||
} | ||
// `skip_binding` is set by runtime/internal `bind()` function only at first call | ||
// this prevents child -> parent backflow that triggers unnecessary $$.update (#4265) | ||
// the flag is used to detech reactive mutation to object in `child.$$.update` | ||
// ignore this flag when parent_value !== child_value | ||
const body = b` | ||
function ${id}(${params}) { | ||
${invalidate_binding} | ||
function ${id}(#skip_binding, ${params}) { | ||
if (!#skip_binding || ${lhs} !== #value) { | ||
${invalidate_binding} | ||
} | ||
Comment on lines
+369
to
+371
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Case 2: Child does not touch passed-in prop value, regardless it's primitive or object, backflow is unnecessary |
||
} | ||
`; | ||
component.partly_hoisted.push(body); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<script> | ||
export let testcase; | ||
export let value = { foo: 'kid' }; | ||
|
||
if (testcase === 'init_update') { | ||
value = { foo: 'kid' } | ||
} | ||
|
||
if (testcase === 'init_mutate') { | ||
value.foo = 'kid' | ||
} | ||
|
||
$: if (testcase === 'reactive_update') { | ||
value = { foo: 'kid' } | ||
} | ||
|
||
$: if (testcase === 'reactive_mutate') { | ||
value.foo = 'kid' | ||
} | ||
|
||
export let updates = []; | ||
$: updates = [...updates, value]; | ||
</script> | ||
|
||
<div>child: {value?.foo} | updates: {updates.length}</div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<script> | ||
import Child from './Child.svelte'; | ||
export let child; | ||
|
||
export let testcase; | ||
export let value; | ||
export let updates = []; | ||
$: updates = [...updates, value]; | ||
</script> | ||
|
||
<div>parent: {value?.foo} | updates: {updates.length}</div> | ||
<Child bind:value bind:this={child} {testcase} /> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
export default { | ||
get props() { | ||
return { | ||
configs: [ | ||
{ testcase: 'parent_override_child_default', value: { foo: 'mon' } }, | ||
{ testcase: 'child_default_populate_parent', value: undefined }, | ||
{ testcase: 'reactive_update', value: { foo: 'mon' } }, | ||
{ testcase: 'reactive_mutate', value: { foo: 'mon' } }, | ||
{ testcase: 'init_update', value: { foo: 'mon' } }, | ||
{ testcase: 'init_mutate', value: { foo: 'mon' } } | ||
] | ||
}; | ||
}, | ||
|
||
async test({ assert, component }) { | ||
const parents = component.parents; | ||
|
||
// first testcase should update once | ||
// the rest should update twice | ||
let p; | ||
p = parents['parent_override_child_default']; | ||
assert.deepEqual(p.value, { foo: 'mon' }); | ||
assert.equal(p.updates.length, 1); | ||
|
||
p = parents['child_default_populate_parent']; | ||
assert.deepEqual(p.value, { foo: 'kid' }); | ||
assert.equal(p.updates.length, 2); | ||
|
||
p = parents['reactive_update']; | ||
assert.deepEqual(p.value, { foo: 'kid' }); | ||
assert.equal(p.updates.length, 2); | ||
|
||
p = parents['reactive_mutate']; | ||
assert.deepEqual(p.value, { foo: 'kid' }); | ||
assert.equal(p.updates.length, 2); | ||
|
||
p = parents['init_update']; | ||
assert.deepEqual(p.value, { foo: 'kid' }); | ||
assert.equal(p.updates.length, 2); | ||
|
||
p = parents['init_mutate']; | ||
assert.deepEqual(p.value, { foo: 'kid' }); | ||
assert.equal(p.updates.length, 2); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<script> | ||
import Parent from './Parent.svelte'; | ||
export let configs = []; | ||
export let parents = {}; | ||
</script> | ||
|
||
{#each configs as config} | ||
<Parent bind:this={parents[config.testcase]} value={config.value} testcase={config.testcase} /> | ||
{/each} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Compiler also inserts
$$invalidate
around exported variables at instancemain_execution_context
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Conduitry / @tanhauhau is this ok to add invalidation in this case, too? Why's invalidation generally forbidden in the main execution context?