Skip to content
全部文档

变形(Morphing)

当 Livewire 组件更新浏览器 DOM 时,会以一种智能方式进行,我们称之为「变形(morphing)」。morph(变形)与 replace(替换)相对。

组件每次更新时,Livewire 不会用新渲染的 HTML 整段替换 组件 HTML,而是动态比较当前 HTML 与新 HTML,找出差异,只在需要变更的地方做精确修改。

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

变形如何工作

要理解 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 变形 成新渲染的 HTML。下面的可视化应能直观说明其工作方式:

观看演示视频:变形基础 该演示视频由 Vimeo 限制为仅可在 Livewire 官方站点嵌入。点击后将在新标签页打开官方文档观看。

可以看到,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>

若用户提交表单时遇到验证错误,会出现如下问题:

观看演示视频:插入中间元素时的问题 该演示视频由 Vimeo 限制为仅可在 Livewire 官方站点嵌入。点击后将在新标签页打开官方文档观看。

可以看到,当 Livewire 遇到错误信息对应的新 <div> 时,它不知道该就地修改现有的 <div>,还是在中间插入新的 <div>

更明确地复述一下发生了什么:

  • Livewire 在两棵树中都遇到第一个 <div>。它们相同,于是继续。
  • Livewire 在两棵树中都遇到第二个 <div>,并认为它们是同一个 <div>,只是内容变了。因此它没有把错误信息作为新元素插入,而是把 <button> 改成了错误信息。
  • 在误改了前一个元素之后,Livewire 又发现比较末尾多了一个元素,于是创建并追加到前一个元素之后。
  • 结果是:本应只是移动的元素,却被销毁再重新创建。

几乎所有与变形相关的 bug 都源于这类场景。

这些 bug 可能导致的具体影响包括:

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

幸运的是,Livewire 已通过以下方式努力缓解这些问题:

内部前瞻

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

这在许多情况下能避免上述场景发生。

下面是「前瞻」算法运行时的可视化:

观看演示视频:内部前瞻算法 该演示视频由 Vimeo 限制为仅可在 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,

包裹条件语句

若上述两种方案仍无法覆盖你的情况,避免变形问题最可靠的做法是:把条件语句和循环包在始终存在的自有元素中。

例如,把上面的 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>Save</button>
    </div>
</form>

条件语句被包在持久元素中后,Livewire 就能正确变形这两棵不同的 HTML 树。

绕过变形

若需要完全绕过某个元素的变形,可使用 wire:replace,让 Livewire 替换该元素的所有子节点,而不是尝试变形现有元素。

另见