Skip to content

Morphing

当 Livewire 组件更新浏览器的 DOM 时,它以一种我们称之为"morphing"的智能方式进行。morph 这个术语与 replace 这样的词形成对比。

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

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

Morphing 的工作原理

要了解 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 wire:key="{{ $loop->index }}">{{ $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 morph 为新渲染的 HTML。以下可视化应该能让您直观地了解它的工作原理:

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

Morphing 的缺陷

以下是 morphing 算法无法正确识别 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>保存</button>
    </div>
</form>

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

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

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

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

这种场景是几乎所有与 morph 相关的 bug 的根源。

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

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

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

内部前瞻

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

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

以下是"前瞻"算法的可视化演示:

注入 morph 标记

在后端,Livewire 会自动检测 Blade 模板中的条件语句,并用 HTML 注释标记将它们包裹起来,Livewire 的 JavaScript 可以在 morphing 时将其用作指南。

以下是之前的 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>保存</button>
    </div>
</form>

有了这些注入到模板中的标记,Livewire 现在可以更容易地检测更改和添加之间的区别。

此功能对 Livewire 应用程序非常有益,但由于它需要通过正则表达式解析模板,有时可能无法正确检测条件语句。如果此功能对您的应用程序弊大于利,您可以在应用程序的 config/livewire.php 文件中使用以下配置禁用它:

php
'inject_morph_markers' => false,

包裹条件语句

如果上述两种解决方案无法覆盖您的情况,避免 morphing 问题最可靠的方法是将条件语句和循环包裹在始终存在的自己的元素中。

例如,以下是上面的 Blade 模板用包裹的 <div> 元素重写后的样子:

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>保存</button>
    </div>
</form>

现在条件语句已被包裹在一个持久元素中,Livewire 将正确地 morph 两个不同的 HTML 树。

绕过 morphing

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

另请参阅

  • Hydration — 了解 Livewire 的请求生命周期
  • 组件 — 组件如何渲染和更新
  • wire:replace — 绕过特定元素的 morphing