-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
fix: use state instead of source in reactive classes
#16239
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
Changes from 4 commits
de8053e
2e69ed4
fb627c0
2ea891e
5670a21
f261686
e3072c9
3ab41d6
69ebe29
b208f71
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 |
|---|---|---|
| @@ -1,9 +1,10 @@ | ||
| /** @import { Source } from '#client' */ | ||
| /** @import { Reaction, Source } from '#client' */ | ||
| import { DEV } from 'esm-env'; | ||
| import { set, source, state } from '../internal/client/reactivity/sources.js'; | ||
| import { label, tag } from '../internal/client/dev/tracing.js'; | ||
| import { get } from '../internal/client/runtime.js'; | ||
| import { active_reaction, get, push_reaction_value } from '../internal/client/runtime.js'; | ||
| import { increment } from './utils.js'; | ||
| import { teardown } from '../internal/client/reactivity/effects.js'; | ||
|
|
||
| /** | ||
| * A reactive version of the built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object. | ||
|
|
@@ -56,13 +57,24 @@ export class SvelteMap extends Map { | |
| #sources = new Map(); | ||
| #version = state(0); | ||
| #size = state(0); | ||
| /**@type {Reaction | null} */ | ||
| #initial_reaction = null; | ||
|
|
||
| /** | ||
| * @param {Iterable<readonly [K, V]> | null | undefined} [value] | ||
| */ | ||
| constructor(value) { | ||
| super(); | ||
|
|
||
| if (active_reaction !== null) { | ||
| this.#initial_reaction = active_reaction; | ||
| // since we only need `initial_reaction` as long as we are in a derived/effect we can | ||
| // safely create a teardown function that will reset it to null and allow for GC | ||
| teardown(() => { | ||
| this.#initial_reaction = null; | ||
| }); | ||
|
||
| } | ||
|
|
||
| if (DEV) { | ||
| // If the value is invalid then the native exception will fire here | ||
| value = new Map(value); | ||
|
|
@@ -79,6 +91,22 @@ export class SvelteMap extends Map { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * If the active_reaction is the same as the reaction that created this SvelteMap, | ||
| * we use state so that it will not be a dependency of the reaction. Otherwise we | ||
| * use source so it will be. | ||
| * | ||
| * @template T | ||
| * @param {T} value | ||
| * @returns {Source<T>} | ||
| */ | ||
| #source(value) { | ||
| if (this.#initial_reaction === active_reaction) { | ||
| return state(value); | ||
| } | ||
| return source(value); | ||
| } | ||
|
|
||
| /** @param {K} key */ | ||
| has(key) { | ||
| var sources = this.#sources; | ||
|
|
@@ -87,7 +115,7 @@ export class SvelteMap extends Map { | |
| if (s === undefined) { | ||
| var ret = super.get(key); | ||
| if (ret !== undefined) { | ||
| s = source(0); | ||
| s = this.#source(0); | ||
|
|
||
| if (DEV) { | ||
| tag(s, `SvelteMap get(${label(key)})`); | ||
|
|
@@ -123,7 +151,7 @@ export class SvelteMap extends Map { | |
| if (s === undefined) { | ||
| var ret = super.get(key); | ||
| if (ret !== undefined) { | ||
| s = source(0); | ||
| s = this.#source(0); | ||
|
|
||
| if (DEV) { | ||
| tag(s, `SvelteMap get(${label(key)})`); | ||
|
|
@@ -154,7 +182,7 @@ export class SvelteMap extends Map { | |
| var version = this.#version; | ||
|
|
||
| if (s === undefined) { | ||
| s = source(0); | ||
| s = state(0); | ||
|
|
||
| if (DEV) { | ||
| tag(s, `SvelteMap get(${label(key)})`); | ||
|
|
@@ -219,8 +247,7 @@ export class SvelteMap extends Map { | |
| if (this.#size.v !== sources.size) { | ||
| for (var key of super.keys()) { | ||
| if (!sources.has(key)) { | ||
| var s = source(0); | ||
|
|
||
| var s = this.#source(0); | ||
| if (DEV) { | ||
| tag(s, `SvelteMap get(${label(key)})`); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,101 @@ | ||
| <script> | ||
| import { SvelteMap } from 'svelte/reactivity'; | ||
|
|
||
| let visibleExternal = $state(false); | ||
| let external = new SvelteMap(); | ||
| const throws = $derived.by(() => { | ||
| external.set(1, 1); | ||
| return external; | ||
| let outside_basic = $state(false); | ||
| let outside_basic_map = new SvelteMap(); | ||
| const throw_basic = $derived.by(() => { | ||
| outside_basic_map.set(1, 1); | ||
| return outside_basic_map; | ||
| }); | ||
|
|
||
| let visibleInternal = $state(false); | ||
| const works = $derived.by(() => { | ||
| let internal = new SvelteMap(); | ||
| internal.set(1, 1); | ||
| return internal; | ||
| let inside_basic = $state(false); | ||
| const works_basic = $derived.by(() => { | ||
| let inside = new SvelteMap(); | ||
| inside.set(1, 1); | ||
| return inside; | ||
| }); | ||
|
|
||
| let outside_has = $state(false); | ||
| let outside_has_map = new SvelteMap([[1, 1]]); | ||
| const throw_has = $derived.by(() => { | ||
| outside_has_map.has(1); | ||
| outside_has_map.set(1, 2); | ||
| return outside_has_map; | ||
| }); | ||
|
|
||
| let inside_has = $state(false); | ||
| const works_has = $derived.by(() => { | ||
| let inside = new SvelteMap([[1, 1]]); | ||
| inside.has(1); | ||
| inside.set(1, 1); | ||
| return inside; | ||
| }); | ||
|
|
||
| let outside_get = $state(false); | ||
| let outside_get_map = new SvelteMap([[1, 1]]); | ||
| const throw_get = $derived.by(() => { | ||
| outside_get_map.get(1); | ||
| outside_get_map.set(1, 2); | ||
| return outside_get_map; | ||
| }); | ||
|
|
||
| let inside_get = $state(false); | ||
| const works_get = $derived.by(() => { | ||
| let inside = new SvelteMap([[1, 1]]); | ||
| inside.get(1); | ||
| inside.set(1, 1); | ||
| return inside; | ||
| }); | ||
|
|
||
| let outside_values = $state(false); | ||
| let outside_values_map = new SvelteMap([[1, 1]]); | ||
| const throw_values = $derived.by(() => { | ||
| outside_values_map.values(1); | ||
| outside_values_map.set(1, 2); | ||
| return outside_values_map; | ||
| }); | ||
|
|
||
| let inside_values = $state(false); | ||
| const works_values = $derived.by(() => { | ||
| let inside = new SvelteMap([[1, 1]]); | ||
| inside.values(); | ||
| inside.set(1, 1); | ||
| return inside; | ||
| }); | ||
| </script> | ||
|
|
||
| <button onclick={() => (visibleExternal = true)}>external</button> | ||
| {#if visibleExternal} | ||
| {throws} | ||
| <button onclick={() => (outside_basic = true)}>external</button> | ||
| {#if outside_basic} | ||
| {throw_basic} | ||
| {/if} | ||
| <button onclick={() => (inside_basic = true)}>internal</button> | ||
| {#if inside_basic} | ||
| {works_basic} | ||
| {/if} | ||
|
|
||
| <button onclick={() => (outside_has = true)}>external</button> | ||
| {#if outside_has} | ||
| {throw_has} | ||
| {/if} | ||
| <button onclick={() => (visibleInternal = true)}>internal</button> | ||
| {#if visibleInternal} | ||
| {works} | ||
| <button onclick={() => (inside_has = true)}>internal</button> | ||
| {#if inside_has} | ||
| {works_has} | ||
| {/if} | ||
|
|
||
| <button onclick={() => (outside_get = true)}>external</button> | ||
| {#if outside_get} | ||
| {throw_get} | ||
| {/if} | ||
| <button onclick={() => (inside_get = true)}>internal</button> | ||
| {#if inside_get} | ||
| {works_get} | ||
| {/if} | ||
|
|
||
| <button onclick={() => (outside_values = true)}>external</button> | ||
| {#if outside_values} | ||
| {throw_values} | ||
| {/if} | ||
| <button onclick={() => (inside_values = true)}>internal</button> | ||
| {#if inside_values} | ||
| {works_values} | ||
| {/if} |
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.
Wondering if there are cases when map still lives, while the reaction could have been GCed and this would prevent it?
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.
Yeah I was wondering about that too...technically it shouldn't be the case because either there's no active reaction (it's outside of a derived/effect) or it's inside it but this means that when the derived/effect is no longer used the function should be GCd and everything inside it too...but I need to do a better check
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.
Maybe this can become
initial_reaction = WeakSet(active_reaction)and we can check withthis.initial_reaction.has(active_reaction)?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.
Just for the sake of history, I've also discovered there is a WeakRef struct that could be alternative here. In case of any future issues might be a possible alternative implementation