Merge branch 'main' into rethink-props

pull/9826/head
Rich Harris 3 years ago
commit 12d6c37e1c

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reuse existing proxy when object has multiple references

@ -409,10 +409,10 @@ export function analyze_component(root, options) {
analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements); analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
} }
// warn on any nonstate declarations that are a) reassigned and mutated and b) referenced in the template // warn on any nonstate declarations that are a) reassigned and b) referenced in the template
for (const scope of [module.scope, instance.scope]) { for (const scope of [module.scope, instance.scope]) {
outer: for (const [name, binding] of scope.declarations) { outer: for (const [name, binding] of scope.declarations) {
if (binding.kind === 'normal' && binding.reassigned && binding.mutated) { if (binding.kind === 'normal' && binding.reassigned) {
for (const { path } of binding.references) { for (const { path } of binding.references) {
if (path[0].type !== 'Fragment') continue; if (path[0].type !== 'Fragment') continue;
for (let i = 1; i < path.length; i += 1) { for (let i = 1; i < path.length; i += 1) {

@ -17,7 +17,7 @@ import {
object_keys object_keys
} from '../utils.js'; } from '../utils.js';
/** @typedef {{ s: Map<string | symbol, import('../types.js').SourceSignal<any>>; v: import('../types.js').SourceSignal<number>; a: boolean, i: boolean }} Metadata */ /** @typedef {{ s: Map<string | symbol, import('../types.js').SourceSignal<any>>; v: import('../types.js').SourceSignal<number>; a: boolean, i: boolean, p: StateObject }} Metadata */
/** @typedef {Record<string | symbol, any> & { [STATE_SYMBOL]: Metadata }} StateObject */ /** @typedef {Record<string | symbol, any> & { [STATE_SYMBOL]: Metadata }} StateObject */
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');
@ -35,15 +35,23 @@ const is_frozen = Object.isFrozen;
* @returns {T} * @returns {T}
*/ */
export function proxy(value, immutable = true) { export function proxy(value, immutable = true) {
if (typeof value === 'object' && value != null && !is_frozen(value) && !(STATE_SYMBOL in value)) { if (typeof value === 'object' && value != null && !is_frozen(value)) {
if (STATE_SYMBOL in value) {
return /** @type {T} */ (value[STATE_SYMBOL].p);
}
const prototype = get_prototype_of(value); const prototype = get_prototype_of(value);
// TODO handle Map and Set as well // TODO handle Map and Set as well
if (prototype === object_prototype || prototype === array_prototype) { if (prototype === object_prototype || prototype === array_prototype) {
define_property(value, STATE_SYMBOL, { value: init(value, immutable), writable: false }); const proxy = new Proxy(value, handler);
define_property(value, STATE_SYMBOL, {
value: init(value, proxy, immutable),
writable: false
});
// @ts-expect-error not sure how to fix this // @ts-expect-error not sure how to fix this
return new Proxy(value, handler); return proxy;
} }
} }
@ -102,15 +110,17 @@ export function unstate(value) {
/** /**
* @param {StateObject} value * @param {StateObject} value
* @param {StateObject} proxy
* @param {boolean} immutable * @param {boolean} immutable
* @returns {Metadata} * @returns {Metadata}
*/ */
function init(value, immutable) { function init(value, proxy, immutable) {
return { return {
s: new Map(), s: new Map(),
v: source(0), v: source(0),
a: is_array(value), a: is_array(value),
i: immutable i: immutable,
p: proxy
}; };
} }

@ -42,7 +42,7 @@ export function readonly(value) {
*/ */
const readonly_error = (_, prop) => { const readonly_error = (_, prop) => {
throw new Error( throw new Error(
`Props cannot be mutated, unless used with \`bind:\`. Use \`bind:prop-in-question={..}\` to make \`${prop}\` settable. Fallback values can never be mutated.` `Non-bound props cannot be mutated — use \`bind:<prop>={...}\` to make \`${prop}\` settable. Fallback values can never be mutated.`
); );
}; };

@ -377,10 +377,7 @@ export function class_name(dom, value) {
* @returns {void} * @returns {void}
*/ */
export function text_effect(dom, value) { export function text_effect(dom, value) {
render_effect(() => { render_effect(() => text(dom, value()));
const string = value();
text(dom, string);
});
} }
/** /**

@ -15,5 +15,5 @@ export default test({
}, },
runtime_error: runtime_error:
'Props cannot be mutated, unless used with `bind:`. Use `bind:prop-in-question={..}` to make `count` settable. Fallback values can never be mutated.' 'Non-bound props cannot be mutated — use `bind:<prop>={...}` to make `count` settable. Fallback values can never be mutated.'
}); });

@ -15,5 +15,5 @@ export default test({
}, },
runtime_error: runtime_error:
'Props cannot be mutated, unless used with `bind:`. Use `bind:prop-in-question={..}` to make `count` settable. Fallback values can never be mutated.' 'Non-bound props cannot be mutated — use `bind:<prop>={...}` to make `count` settable. Fallback values can never be mutated.'
}); });

@ -0,0 +1,30 @@
import { test } from '../../test';
export default test({
html: `
<button>0</button>
<button>0</button>
`,
async test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');
await btn1?.click();
assert.htmlEqual(
target.innerHTML,
`
<button>1</button>
<button>1</button>
`
);
await btn2?.click();
assert.htmlEqual(
target.innerHTML,
`
<button>2</button>
<button>2</button>
`
);
}
});

@ -0,0 +1,14 @@
<script>
let obj = { count: 0 };
let a = $state(obj);
let b = $state(obj);
</script>
<button onclick={() => a.count += 1}>
{a.count}
</button>
<button onclick={() => b.count += 1}>
{b.count}
</button>
Loading…
Cancel
Save