Skip to content

DOM 变形(Morphing)

当 Livewire 组件更新浏览器的 DOM 时,它以一种我们称为"变形"(morphing)的智能方式进行。术语 morphreplace(替换)一词形成对比。

Livewire 不是在每次更新组件时用新渲染的 HTML _替换_组件的 HTML,而是动态比较当前 HTML 与新 HTML,识别差异,并仅在需要更改的地方对 HTML 进行精确更改。

这样做的好处是保留组件上现有的、未更改的元素。例如,事件监听器、焦点状态和表单输入值都在 Livewire 更新之间得以保留。当然,与每次更新时清除并重新渲染新 DOM 相比,变形还提供了更好的性能。

变形的工作原理

要了解 Livewire 如何确定在 Livewire 请求之间更新哪些元素,请考虑这个简单的 Todos 组件:

php
class Todos extends Component
{
    public $todo = '';

    public $todos = [
        'first',
        'second',
    ];

    public function add()
    {
        $this->todos[] = $this->todo;
    }
}
blade
<form wire:submit="add">
    <ul>
        @foreach ($todos as $item)
            <li>{{ $item }}</li>
        @endforeach
    </ul>

    <input wire:model="todo">
</form>

此组件的初始渲染将输出以下 HTML:

html
<form wire:submit="add">
    <ul>
        <li>first</li>

        <li>second</li>
    </ul>

    <input wire:model="todo">
</form>

现在,想象你在输入字段中键入"third"并按下 [Enter] 键。新渲染的 HTML 将是:

html
<form wire:submit="add">
    <ul>
        <li>first</li>

        <li>second</li>

        <li>third</li> <!-- [tl! add] -->
    </ul>

    <input wire:model="todo">
</form>

当 Livewire 处理组件更新时,它将原始 DOM _变形_为新渲染的 HTML。以下可视化应该直观地让你了解它是如何工作的:

如你所见,Livewire 同时遍历两个 HTML 树。当它在两个树中遇到每个元素时,它会比较它们的更改、添加和删除。如果检测到一个,它会精确地进行适当的更改。

变形的缺陷

以下是变形算法无法正确识别 HTML 树中的更改从而导致应用程序出现问题的场景。

插入中间元素

考虑以下虚构的 CreatePost 组件的 Livewire Blade 模板:

blade
<form wire:submit="save">
    <div>
        <input wire:model="title">
    </div>

    @if ($errors->has('title'))
        <div>{{ $errors->first('title') }}</div>
    @endif

    <div>
        <button>Save</button>
    </div>
</form>

如果用户尝试提交表单,但遇到验证错误,则会发生以下问题:

如你所见,当 Livewire 遇到错误消息的新 <div> 时,它不知道是就地更改现有的 <div>,还是在中间插入新的 <div>

更明确地重申正在发生的事情:

  • Livewire 在两个树中遇到第一个 <div>。它们是相同的,所以它继续。
  • Livewire 在两个树中遇到第二个 <div> 并认为它们是同一个 <div>,只是一个改变了内容。因此,它不是将错误消息作为新元素插入,而是将 <button> 更改为错误消息。
  • 然后,Livewire 在错误地修改了前一个元素之后,注意到比较结束时有一个额外的元素。然后它在前一个元素之后创建并附加该元素。
  • 因此,破坏然后重新创建一个本应该只是移动的元素。

这种场景是几乎所有与变形相关的错误的根源。

以下是这些错误的一些具体问题影响:

  • 事件监听器和元素状态在更新之间丢失
  • 事件监听器和状态被错误地放置在错误的元素上
  • 整个 Livewire 组件可以被重置或重复,因为 Livewire 组件也只是 DOM 树中的元素
  • Alpine 组件和状态可能会丢失或错位

幸运的是,Livewire 努力使用以下方法来缓解这些问题:

内部前瞻

Livewire 在其变形算法中有一个额外的步骤,在更改元素之前检查后续元素及其内容。

这在许多情况下防止了上述场景的发生。

以下是"前瞻"算法实际运行的可视化:

注入变形标记

在后端,Livewire 自动检测 Blade 模板中的条件并将它们包装在 HTML 注释标记中,Livewire 的 JavaScript 可以在变形时将其用作指南。

以下是前面的 Blade 模板的示例,但带有 Livewire 注入的标记:

blade
<form wire:submit="save">
    <div>
        <input wire:model="title">
    </div>

    <!--[if BLOCK]><![endif]--> <!-- [tl! highlight] -->
    @if ($errors->has('title'))
        <div>Error: {{ $errors->first('title') }}</div>
    @endif
    <!--[if ENDBLOCK]><![endif]--> <!-- [tl! highlight] -->

    <div>
        <button>Save</button>
    </div>
</form>

将这些标记注入模板后,Livewire 现在可以更轻松地检测更改和添加之间的差异。

此功能对 Livewire 应用程序非常有益,但由于它需要通过正则表达式解析模板,因此有时可能无法正确检测条件。如果此功能对你的应用程序来说是阻碍而不是帮助,你可以在应用程序的 config/livewire.php 文件中使用以下配置禁用它:

php
'inject_morph_markers' => false,

包装条件

如果上述两个解决方案不能涵盖你的情况,避免变形问题的最可靠方法是将条件和循环包装在始终存在的自己的元素中。

例如,以下是使用包装 <div> 元素重写的上述 Blade 模板:

blade
<form wire:submit="save">
    <div>
        <input wire:model="title">
    </div>

    <div> <!-- [tl! highlight] -->
        @if ($errors->has('title'))
            <div>{{ $errors->first('title') }}</div>
        @endif
    </div> <!-- [tl! highlight] -->

    <div>
        <button>Save</button>
    </div>
</form>

现在条件已经被包装在一个持久元素中,Livewire 将正确地变形两个不同的 HTML 树。

绕过变形

如果你需要完全绕过元素的变形,可以使用 wire:replace 来指示 Livewire 替换元素的所有子元素,而不是尝试变形现有元素。