变形(Morphing)
当 Livewire 组件更新浏览器 DOM 时,会以一种智能方式进行,我们称之为「变形(morphing)」。morph(变形)与 replace(替换)相对。
组件每次更新时,Livewire 不会用新渲染的 HTML 整段替换 组件 HTML,而是动态比较当前 HTML 与新 HTML,找出差异,只在需要变更的地方做精确修改。
这样做的好处是能保留组件上未变更的现有元素。例如,事件监听器、焦点状态和表单输入值都会在 Livewire 更新之间保留。当然,与每次清空并重新渲染 DOM 相比,变形也带来更好的性能。
变形如何工作
要理解 Livewire 如何在请求之间决定更新哪些元素,可看这个简单的 Todos 组件:
class Todos extends Component
{
public $todo = '';
public $todos = [
'first',
'second',
];
public function add()
{
$this->todos[] = $this->todo;
}
}<form wire:submit="add">
<ul>
@foreach ($todos as $item)
<li wire:key="{{ $loop->index }}">{{ $item }}</li>
@endforeach
</ul>
<input wire:model="todo">
</form>该组件的初始渲染会输出如下 HTML:
<form wire:submit="add">
<ul>
<li>first</li>
<li>second</li>
</ul>
<input wire:model="todo">
</form>现在假设你在输入框里输入了「third」并按下 [Enter]。新渲染的 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 模板:
<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 标记后的示例:
<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 中用如下配置关闭:
'inject_morph_markers' => false,包裹条件语句
若上述两种方案仍无法覆盖你的情况,避免变形问题最可靠的做法是:把条件语句和循环包在始终存在的自有元素中。
例如,把上面的 Blade 模板改写为用 <div> 包裹:
<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 替换该元素的所有子节点,而不是尝试变形现有元素。
另见
- Hydration — 了解 Livewire 的请求生命周期
- Components — 组件如何渲染与更新
- wire:replace — 对特定元素绕过变形