如何将 Svelte 商店与树状嵌套对象一起使用?
How to use Svelte store with tree-like nested object?
Svelte官方教程在its document for <svelte:self>
中使用了如此复杂的对象
let root = [
{
type: 'folder',
name: 'Important work stuff',
files: [
{ type: 'file', name: 'quarterly-results.xlsx' }
]
},
{
type: 'folder',
name: 'Animal GIFs',
files: [
{
type: 'folder',
name: 'Dogs',
files: [
{ type: 'file', name: 'treadmill.gif' },
{ type: 'file', name: 'rope-jumping.gif' }
]
},
{
type: 'folder',
name: 'Goats',
files: [
{ type: 'file', name: 'parkour.gif' },
{ type: 'file', name: 'rampage.gif' }
]
},
{ type: 'file', name: 'cat-roomba.gif' },
{ type: 'file', name: 'duck-shuffle.gif' },
{ type: 'file', name: 'monkey-on-a-pig.gif' }
]
},
{ type: 'file', name: 'TODO.md' }
];
如果这个对象需要反应并放在商店里,应该怎么做?树应该包装为单个存储,还是每个文件和文件夹都是自己的存储并相应地嵌套存储?
在这两种情况下,似乎只要顶级属性发生更改(svelte store 认为对象的更新总是新鲜的),就会检查整个树是否有更改?
需要知道的几件事...
商店的 $
前缀表示法也适用于 分配 一个新值给可写商店:
<script>
import { writable } from 'svelte/store'
const x = writable(0)
const onClick = () => {
$x = $x + 1
}
</script>
<button on:click={onClick}>+</button>
<span>{$x}</span>
这也适用于写入对象的单个道具或数组中的单个项目:
<script>
import { writable } from 'svelte/store'
const x = writable({
count: 0,
})
const onClick = () => {
$x.count = $x.count + 1
}
</script>
<button on:click={onClick}>+</button>
<span>{$x.count}</span>
从父组件,您可以将变量绑定到子组件的 prop:
Child.svelte
<script>
export let value
</script>
<input bind:value />
App.svelte
<script>
import Child from './Child.svelte'
let value = ''
$: console.log(value)
</script>
<Child bind:value />
注意:绑定仅在 相同变量 时有效。也就是说,您不能将绑定变量放在中间变量中,并让 Svelte 继续跟踪此绑定。 Svelte 确实会继续跟踪对象的各个属性(只要它们是从最初绑定的变量中引用的——使用点符号)和数组项,特别是在 {#each}
循环中:
<script>
import { writable } from 'svelte/store'
const x = writable({
count: 0,
})
const y = writable([
{ count: 0 },
{ count: 1 },
])
const onClick = () => {
$x.count = $x.count + 1
}
</script>
<button on:click={onClick}>+</button>
<span>{$x.count}</span>
<hr />
{#each $y as item, i}
<div>
<button on:click={() => item.count++}>$y[{i}]: +</button>
</div>
{/each}
<pre>{JSON.stringify($y)}</pre>
所以,了解所有这些,如果您将源数据放在可写存储中并且您对 2 向绑定非常精确,那么您最终可以为您的问题找到一个非常便宜的解决方案......(请参阅在 REPL)
stores.js
import { readable, writable, derived } from 'svelte/store'
// a big writable store
export const root = writable([
{
type: 'folder',
name: 'Important work stuff',
files: [{ type: 'file', name: 'quarterly-results.xlsx' }],
},
{
type: 'folder',
name: 'Animal GIFs',
files: [
{
type: 'folder',
name: 'Dogs',
files: [
{ type: 'file', name: 'treadmill.gif' },
{ type: 'file', name: 'rope-jumping.gif' },
],
},
{
type: 'folder',
name: 'Goats',
files: [
{ type: 'file', name: 'parkour.gif' },
{ type: 'file', name: 'rampage.gif' },
],
},
{ type: 'file', name: 'cat-roomba.gif' },
{ type: 'file', name: 'duck-shuffle.gif' },
{ type: 'file', name: 'monkey-on-a-pig.gif' },
],
},
{ type: 'file', name: 'TODO.md' },
])
App.svelte
<script>
import { root } from './stores.js'
import Folder from './Folder.svelte'
$: console.log($root)
</script>
<div class="hbox">
<div>
<!-- NOTE binding to the store itself: bind=files={root} -->
<Folder readonly expanded bind:files={$root} file={{ name: 'Home' }} />
</div>
<pre>{JSON.stringify($root, null, 2)}</pre>
</div>
<style>
.hbox {
display: flex;
justify-content: space-around;
}
</style>
Folder.svelte
<script>
import File from './File.svelte'
export let readonly = false
export let expanded = false
export let file
export let files
function toggle() {
expanded = !expanded
}
</script>
{#if readonly}
<!-- NOTE bindings must keep referencing the "entry" variable
(here: `file.`) to be tracked -->
<span class:expanded on:click={toggle}>{file.name}</span>
{:else}
<label>
<span class:expanded on:click={toggle} />
<input bind:value={file.name} />
</label>
{/if}
{#if expanded}
<ul>
{#each files as file}
<li>
{#if file.type === 'folder'}
<!-- NOTE the intermediate variable created by the #each loop
(here: local `file` variable) preserves tracking, though -->
<svelte:self bind:file bind:files={file.files} />
{:else}
<File bind:file />
{/if}
</li>
{/each}
</ul>
{/if}
<style>
span {
padding: 0 0 0 1.5em;
background: url(tutorial/icons/folder.svg) 0 0.1em no-repeat;
background-size: 1em 1em;
font-weight: bold;
cursor: pointer;
min-height: 1em;
display: inline-block;
}
.expanded {
background-image: url(tutorial/icons/folder-open.svg);
}
ul {
padding: 0.2em 0 0 0.5em;
margin: 0 0 0 0.5em;
list-style: none;
border-left: 1px solid #eee;
}
li {
padding: 0.2em 0;
}
</style>
File.svelte
<script>
export let file
$: type = file.name.slice(file.name.lastIndexOf('.') + 1)
</script>
<label>
<span style="background-image: url(tutorial/icons/{type}.svg)" />
<input bind:value={file.name} />
</label>
<style>
span {
padding: 0 0 0 1.5em;
background: 0 0.1em no-repeat;
background-size: 1em 1em;
}
</style>
但是请注意,这可能不是最有效的解决方案。
原因是对商店任何部分的任何更改都将被检测为对整个商店的更改,因此 Svelte 必须将更改传播并重新验证到每个消费者(组件)或此数据.我们不一定要谈论一些繁重的处理,因为 Svelte 仍然知道数据图,并且会很早就用非常便宜和针对外科手术的 if
测试来短路大部分传播。但是,处理的复杂性仍然会随着存储中对象的大小线性增长(尽管缓慢)。
在某些情况下,数据可能非常大或其他情况(可能允许延迟获取嵌套节点?),您可能需要详细说明上述示例中演示的技术。例如,您可以通过将数据中的递归节点(即上例中的 files
属性)包装在可写存储中来限制处理更改的算法复杂性(成本)。是的,那将是商店中的商店(高级商店?)。连接在一起肯定有点微妙,但理论上这会给你近乎无限的可伸缩性,因为每个更改只会传播到受影响节点的兄弟节点,而不是整个树。
自己注意:
https://github.com/sveltejs/svelte/issues/1435#issuecomment-735233175
Start with a single store with all your global state and then split
off views from that main store. As a proof of concept I have written a
tool called subStore.
Examples and links to repl can be found here
https://github.com/bradphelan/immer.loves.svelte
Svelte官方教程在its document for <svelte:self>
let root = [
{
type: 'folder',
name: 'Important work stuff',
files: [
{ type: 'file', name: 'quarterly-results.xlsx' }
]
},
{
type: 'folder',
name: 'Animal GIFs',
files: [
{
type: 'folder',
name: 'Dogs',
files: [
{ type: 'file', name: 'treadmill.gif' },
{ type: 'file', name: 'rope-jumping.gif' }
]
},
{
type: 'folder',
name: 'Goats',
files: [
{ type: 'file', name: 'parkour.gif' },
{ type: 'file', name: 'rampage.gif' }
]
},
{ type: 'file', name: 'cat-roomba.gif' },
{ type: 'file', name: 'duck-shuffle.gif' },
{ type: 'file', name: 'monkey-on-a-pig.gif' }
]
},
{ type: 'file', name: 'TODO.md' }
];
如果这个对象需要反应并放在商店里,应该怎么做?树应该包装为单个存储,还是每个文件和文件夹都是自己的存储并相应地嵌套存储?
在这两种情况下,似乎只要顶级属性发生更改(svelte store 认为对象的更新总是新鲜的),就会检查整个树是否有更改?
需要知道的几件事...
商店的 $
前缀表示法也适用于 分配 一个新值给可写商店:
<script>
import { writable } from 'svelte/store'
const x = writable(0)
const onClick = () => {
$x = $x + 1
}
</script>
<button on:click={onClick}>+</button>
<span>{$x}</span>
这也适用于写入对象的单个道具或数组中的单个项目:
<script>
import { writable } from 'svelte/store'
const x = writable({
count: 0,
})
const onClick = () => {
$x.count = $x.count + 1
}
</script>
<button on:click={onClick}>+</button>
<span>{$x.count}</span>
从父组件,您可以将变量绑定到子组件的 prop:
Child.svelte
<script>
export let value
</script>
<input bind:value />
App.svelte
<script>
import Child from './Child.svelte'
let value = ''
$: console.log(value)
</script>
<Child bind:value />
注意:绑定仅在 相同变量 时有效。也就是说,您不能将绑定变量放在中间变量中,并让 Svelte 继续跟踪此绑定。 Svelte 确实会继续跟踪对象的各个属性(只要它们是从最初绑定的变量中引用的——使用点符号)和数组项,特别是在 {#each}
循环中:
<script>
import { writable } from 'svelte/store'
const x = writable({
count: 0,
})
const y = writable([
{ count: 0 },
{ count: 1 },
])
const onClick = () => {
$x.count = $x.count + 1
}
</script>
<button on:click={onClick}>+</button>
<span>{$x.count}</span>
<hr />
{#each $y as item, i}
<div>
<button on:click={() => item.count++}>$y[{i}]: +</button>
</div>
{/each}
<pre>{JSON.stringify($y)}</pre>
所以,了解所有这些,如果您将源数据放在可写存储中并且您对 2 向绑定非常精确,那么您最终可以为您的问题找到一个非常便宜的解决方案......(请参阅在 REPL)
stores.js
import { readable, writable, derived } from 'svelte/store'
// a big writable store
export const root = writable([
{
type: 'folder',
name: 'Important work stuff',
files: [{ type: 'file', name: 'quarterly-results.xlsx' }],
},
{
type: 'folder',
name: 'Animal GIFs',
files: [
{
type: 'folder',
name: 'Dogs',
files: [
{ type: 'file', name: 'treadmill.gif' },
{ type: 'file', name: 'rope-jumping.gif' },
],
},
{
type: 'folder',
name: 'Goats',
files: [
{ type: 'file', name: 'parkour.gif' },
{ type: 'file', name: 'rampage.gif' },
],
},
{ type: 'file', name: 'cat-roomba.gif' },
{ type: 'file', name: 'duck-shuffle.gif' },
{ type: 'file', name: 'monkey-on-a-pig.gif' },
],
},
{ type: 'file', name: 'TODO.md' },
])
App.svelte
<script>
import { root } from './stores.js'
import Folder from './Folder.svelte'
$: console.log($root)
</script>
<div class="hbox">
<div>
<!-- NOTE binding to the store itself: bind=files={root} -->
<Folder readonly expanded bind:files={$root} file={{ name: 'Home' }} />
</div>
<pre>{JSON.stringify($root, null, 2)}</pre>
</div>
<style>
.hbox {
display: flex;
justify-content: space-around;
}
</style>
Folder.svelte
<script>
import File from './File.svelte'
export let readonly = false
export let expanded = false
export let file
export let files
function toggle() {
expanded = !expanded
}
</script>
{#if readonly}
<!-- NOTE bindings must keep referencing the "entry" variable
(here: `file.`) to be tracked -->
<span class:expanded on:click={toggle}>{file.name}</span>
{:else}
<label>
<span class:expanded on:click={toggle} />
<input bind:value={file.name} />
</label>
{/if}
{#if expanded}
<ul>
{#each files as file}
<li>
{#if file.type === 'folder'}
<!-- NOTE the intermediate variable created by the #each loop
(here: local `file` variable) preserves tracking, though -->
<svelte:self bind:file bind:files={file.files} />
{:else}
<File bind:file />
{/if}
</li>
{/each}
</ul>
{/if}
<style>
span {
padding: 0 0 0 1.5em;
background: url(tutorial/icons/folder.svg) 0 0.1em no-repeat;
background-size: 1em 1em;
font-weight: bold;
cursor: pointer;
min-height: 1em;
display: inline-block;
}
.expanded {
background-image: url(tutorial/icons/folder-open.svg);
}
ul {
padding: 0.2em 0 0 0.5em;
margin: 0 0 0 0.5em;
list-style: none;
border-left: 1px solid #eee;
}
li {
padding: 0.2em 0;
}
</style>
File.svelte
<script>
export let file
$: type = file.name.slice(file.name.lastIndexOf('.') + 1)
</script>
<label>
<span style="background-image: url(tutorial/icons/{type}.svg)" />
<input bind:value={file.name} />
</label>
<style>
span {
padding: 0 0 0 1.5em;
background: 0 0.1em no-repeat;
background-size: 1em 1em;
}
</style>
但是请注意,这可能不是最有效的解决方案。
原因是对商店任何部分的任何更改都将被检测为对整个商店的更改,因此 Svelte 必须将更改传播并重新验证到每个消费者(组件)或此数据.我们不一定要谈论一些繁重的处理,因为 Svelte 仍然知道数据图,并且会很早就用非常便宜和针对外科手术的 if
测试来短路大部分传播。但是,处理的复杂性仍然会随着存储中对象的大小线性增长(尽管缓慢)。
在某些情况下,数据可能非常大或其他情况(可能允许延迟获取嵌套节点?),您可能需要详细说明上述示例中演示的技术。例如,您可以通过将数据中的递归节点(即上例中的 files
属性)包装在可写存储中来限制处理更改的算法复杂性(成本)。是的,那将是商店中的商店(高级商店?)。连接在一起肯定有点微妙,但理论上这会给你近乎无限的可伸缩性,因为每个更改只会传播到受影响节点的兄弟节点,而不是整个树。
自己注意:
https://github.com/sveltejs/svelte/issues/1435#issuecomment-735233175
Start with a single store with all your global state and then split off views from that main store. As a proof of concept I have written a tool called subStore. Examples and links to repl can be found here https://github.com/bradphelan/immer.loves.svelte