【问题标题】:How to use Svelte store with tree-like nested object?如何将 Svelte 存储与树状嵌套对象一起使用?
【发布时间】:2021-03-13 11:11:23
【问题描述】:

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' }
];

如果这个对象需要是响应式的并放置在 store 中,应该怎么做?应该将树包装为单个存储,还是每个文件和文件夹都是其自己的存储并相应地嵌套存储?

在这两种情况下,似乎无论何时更改顶级属性(svelte store 认为对象的更新总是新鲜的),都会检查整个树是否有更改?

【问题讨论】:

    标签: svelte svelte-store


    【解决方案1】:

    需要了解的一些事情......

    store 的 $ 前缀符号也可以分配一个新值给可写的 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 路绑定很精确,那么您最终可以为您的问题找到一个非常便宜的解决方案......(参见@ 987654321@)

    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 属性)包装在可写存储中来限制处理更改的算法复杂性(成本)。是的,那将是商店中的商店(高级商店?)。这肯定会有点微妙的连接在一起,但理论上这会给你带来近乎无限的可扩展性,因为每个更改只会传播到受影响节点的兄弟姐妹,而不是整个树。

    【讨论】:

    • 感谢速成课程,但是该示例真正演示了如何直接在 svelte 中使用复杂对象,而不是通过商店,因为您在根目录中展开商店,然后子组件只处理普通对象。
    • 另外,当文件名改变时,商店真的不知道这一点,因为它自己从来没有被通知改变。换句话说,文件名的变化是外界无法观察到的,除非组件发出自定义事件或其他东西,但这意味着存储不会在其中发挥任何作用。
    • 真的吗?什么让你有那个想法?我认为右侧的 JSON 转储表明商店确实被修改了。您是否尝试过手动订阅商店以确认它不知道更改?
    • 抱歉,我应该先尝试一下再下结论。你是对的,svelte store 足够聪明,可以检测到存储数据深处的变化。当数据是树状的时,我猜你一定会递归地向下传递分支。感谢您的详细解释。
    • 我很惊讶!对商店的深入观察记录在哪里?你能提供一个链接吗?确实很有用。
    猜你喜欢
    • 1970-01-01
    • 2018-04-16
    • 1970-01-01
    • 1970-01-01
    • 2019-05-11
    • 2017-06-14
    • 1970-01-01
    • 1970-01-01
    • 2022-09-30
    相关资源
    最近更新 更多