如何让 Svelte 在 DOM 中的其他地方重用组件实例,而不是销毁/重新创建它?

Mid*_*run 6 svelte svelte-component svelte-3

我正在开发一个以图形方式呈现树结构的应用程序。树中有各种节点类型,每种类型都有一个相应的 Svelte 组件来渲染它。

该应用程序的功能之一是您可以单击并拖动树中的节点,并将其移动到不同的父节点。当将节点放到新位置时,底层数据结构会相应地转换,并且 Svelte 会更新视图。

问题是,被移动节点的组件实例被破坏(连同它的所有后代,可能是数百个节点),然后无论节点移动到哪里,整个事物都会从头开始重新创建。这是很多不必要的工作,并且有非常明显的滞后,而它本应该能够立即移动现有的组件实例和关联的 DOM 节点。

有什么方法可以提示 Svelte 应该重用该实例吗?

(每个节点上都有一个唯一的 ID,如果有帮助的话。)

Leo*_*896 1

您无法向 Svelte 暗示它将重用该实例,但您可以创建一个借用系统,将组件安全地移动到其他容器。我已将其打包到一个库中,svelte-reparent.

通过制作原始容器Limbo和渲染器 ,Portals并且能够teleport在这些组件之间进行操作,我们还可以:

  • 将生命周期附加到Limbo(安全地允许返回空间)
  • 添加破坏钩子以Portal安全地将其移回Limbo

由于 Svelte 没有虚拟 DOM,因此移动元素要容易得多:

Limbo.svelte- 我们的容器

<!-- 
    Limbo is a place to initialize the element,
    and serves as a hidden space to keep track of nodes.

    Since Limbo owns the lifecycle of the current element,
    the moment that Limbo gets destroyed, it will destroy its child element
    that it *thinks* it owns. Because of this, it is quite safe to destroy and reinitialize
    a Limbo component without causing unintended side effects to the DOM.
-->

<script lang="ts">
    import { _components } from '$lib/Portal.svelte';
    import { onMount } from 'svelte';

    export let component: HTMLElement;
    let container: HTMLDivElement;

    // Register the component and its limbo
    onMount(() => _components.set(component, { ..._components.get(component), limbo: container }));
</script>

<!-- 
    We don't want to render this component, 
    but we use it as the initial holder before teleporting it.
    This allows us to have a safe fallback
    for when a Portal gets destroyed.

    We also wrap it to guarantee that `component` is a DOM component,
    since we can't guarantee that all svelte components only have 1 root node.
-->
<div style="display: none;" bind:this={container}>
    <div style="display: contents;" bind:this={component}><slot /></div>
</div>
Run Code Online (Sandbox Code Playgroud)

Portal.svelte

<script context="module" lang="ts">
    import { writable } from 'svelte/store';

    type Container = HTMLElement;
    type Key = string | number | symbol;

    /**
     * Universal map to keep track of what portal a component wants to be in,
     * as well as its original limbo owner.
     *
     * DON'T MODIFY EXTERNALLY!
     * Doing so is **undefined behavior**.
     */
    export let _components = new Map<
        Container,
        {
            limbo?: HTMLElement;
            key?: Key;
        }
    >();

    // dirty tracker - a Map isn't reactive, so we need to coerce Svelte to re-render
    let dirty = writable(Symbol());

    export async function teleport(component: Container, key: Key) {
        _components.set(component, { ..._components.get(component), key });

        // trigger a re-render
        dirty.set(Symbol());
    }
</script>

<script lang="ts">
    import { onDestroy } from 'svelte';

    export let key: Key;
    export let component: Container;

    /*
        - component may be nil before mount
        - listen to dirty to force a re-render
    */
    $: if (component && $dirty && _components.get(component)?.key == key) {
        // appendChild forces a move, not a copy - we can safely use this as the DOM
        // handles ownership of the node for us
        container.appendChild(component);
    }

    let container: HTMLDivElement;

    onDestroy(() => {
        // check if we own the component
        const { limbo, key: localKey } = _components.get(component) || {};

        if (localKey !== key) return;
        _components.delete(component);

        // move the component back to the limbo till it gets re-mounted
        limbo?.appendChild(component);

        // trigger a re-render
        dirty.set(Symbol());
    });
</script>

<div style="display: contents;" bind:this={container} />
Run Code Online (Sandbox Code Playgroud)

+page.svelte (example)

<script lang="ts">
    import { onMount } from 'svelte';
    import { Portal, Limbo, teleport } from '$lib';

    let component: HTMLElement;

    function send(label: string) {
        return () => {
            teleport(component, label);
        };
    }

    onMount((): void => send('a')());
</script>

<main>
    <Limbo bind:component>
        <input placeholder="Enter unkept state" />
    </Limbo>
    <div class="container">
        <h1>Container A</h1>
        <Portal key="a" {component} />
        <button on:click={send('a')}>Move Component Here</button>
    </div>
    <div class="container">
        <h1>Container B</h1>
        <Portal key="b" {component} />
        <button on:click={send('b')}>Move Component Here</button>
    </div>
</main>

<style>
    .container {
        border: 1px solid black;
        margin: 1rem;
        padding: 1rem;
    }
</style>

Run Code Online (Sandbox Code Playgroud)

上传的示例