Как использовать Svelte store с древовидным вложенным объектом?

В официальном руководстве Svelte такой сложный объект используется в документе для <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 считает, что обновления из объектов всегда свежие), все дерево будет проверяться на предмет изменений?


person hgl    schedule 01.12.2020    source источник


Ответы (1)


Несколько вещей, которые нужно знать ...

Обозначения префикса $ для хранилищ также работают для назначения нового значения доступному для записи хранилищу:

<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>

Из родительского компонента вы можете привязать переменную к опоре дочернего компонента:

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>

Итак, зная все это, если вы поместите свои исходные данные в хранилище с возможностью записи и вы будете точны в своих двусторонних привязках, вы можете получить довольно дешевое решение своего вопроса ... (см. 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 в приведенном выше примере) каждый в доступное для записи хранилище. Да, это будут магазины в магазинах (магазины высокого уровня?). Это, безусловно, было бы немного деликатно соединить вместе, но теоретически это даст вам почти бесконечную масштабируемость, потому что каждое изменение будет распространяться только на братьев и сестер затронутого узла, а не на все дерево.

person rixo    schedule 02.12.2020
comment
Спасибо за ускоренный курс, однако этот пример действительно демонстрирует, как использовать сложный объект непосредственно в svelte, а не через хранилище, поскольку вы разворачиваете хранилище в корне, а затем дочерние компоненты имеют дело только с простыми объектами. - person hgl; 03.12.2020
comment
Кроме того, когда имя файла изменяется, магазин действительно не знает этого, поскольку сам никогда не уведомляется об изменении. Другими словами, изменение имени файла незаметно для внешнего мира, если только компонент не излучает что-то особенное, но это означает, что магазины не играют в этом никакой роли. - person hgl; 03.12.2020
comment
Действительно? Что заставляет вас думать, что? Я думал, что дамп JSON справа демонстрирует, что хранилище действительно было изменено. Вы пытались подписаться на магазин вручную, чтобы убедиться, что он не знал об изменении? - person rixo; 03.12.2020
comment
Извините, я должен был попробовать, прежде чем делать выводы. Вы правы, svelte store достаточно умен, чтобы обнаруживать изменения глубоко внутри данных магазина. А когда данные древовидные, я думаю, вы обязаны рекурсивно передавать ветви вниз. Спасибо за подробное объяснение. - person hgl; 03.12.2020
comment
Я удивлен! Где задокументировано это глубокое наблюдение за магазином? Можете дать ссылку? Действительно полезно. - person robsch; 08.06.2021
comment
@robsch На самом деле это не особый случай глубокого наблюдения за магазином, это общее поведение, с одной стороны, двусторонней привязки, а с другой стороны, записываемых магазинов и магической нотации хранилища. Глубокое наблюдение возникает из сочетания тех поведений, которые в основном задокументированы и проиллюстрированы отдельно в документах и ​​примерах. Вывод: Svelte дает вам мощные простые атомы и ведет себя предсказуемо, когда вы их составляете. Используйте силу, клей - это ваше мышление, проявите творческий подход! (Хотя будьте осторожны, не заходите слишком далеко, чем проще, тем лучше - как только что проиллюстрировано.) - person rixo; 08.06.2021