Skip to content

深入理解嵌套

像许多其他基于组件的框架一样,Livewire 组件是可嵌套的——这意味着一个组件可以在其内部渲染多个组件。

然而,由于 Livewire 的嵌套系统与其他框架的构建方式不同,有一些重要的影响和约束需要了解。

首先确保您了解 hydration

在进一步了解 Livewire 的嵌套系统之前,完全了解 Livewire 如何水合组件会很有帮助。您可以通过阅读 hydration 文档 了解更多。

每个组件都是独立的

在 Livewire 中,页面上的每个组件都独立于其他组件跟踪其状态和进行更新。

例如,考虑以下 Posts 和嵌套的 ShowPost 组件:

php
<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;

class Posts extends Component
{
    public $postLimit = 2;

    public function render()
    {
        return view('livewire.posts', [
            'posts' => Auth::user()->posts()
                ->limit($this->postLimit)->get(),
        ]);
    }
}
blade
<div>
    文章限制: <input type="number" wire:model.live="postLimit">

    @foreach ($posts as $post)
        <livewire:show-post :$post :wire:key="$post->id">
    @endforeach
</div>
php
<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use App\Models\Post;

class ShowPost extends Component
{
    public Post $post;

    public function render()
    {
        return view('livewire.show-post');
    }
}
blade
<div>
    <h1>{{ $post->title }}</h1>

    <p>{{ $post->content }}</p>

    <button wire:click="$refresh">刷新文章</button>
</div>

以下是整个组件树在初始页面加载时可能的 HTML 样子:

html
<div wire:id="123" wire:snapshot="...">
    文章限制: <input type="number" wire:model.live="postLimit">

    <div wire:id="456" wire:snapshot="...">
        <h1>第一篇文章</h1>

        <p>文章内容</p>

        <button wire:click="$refresh">刷新文章</button>
    </div>

    <div wire:id="789" wire:snapshot="...">
        <h1>第二篇文章</h1>

        <p>文章内容</p>

        <button wire:click="$refresh">刷新文章</button>
    </div>
</div>

注意父组件包含其渲染的模板以及嵌套在其中的所有组件的渲染模板。

因为每个组件都是独立的,它们各自都有自己的 ID 和快照(wire:idwire:snapshot)嵌入在其 HTML 中,供 Livewire 的 JavaScript 核心提取和跟踪。

让我们考虑几个不同的更新场景,看看 Livewire 如何处理不同层级嵌套的差异。

更新子组件

如果您点击其中一个子 show-post 组件中的"刷新文章"按钮,以下内容将被发送到服务器:

js
{
    memo: { name: 'show-post', id: '456' },

    state: { ... },
}

以下是将返回的 HTML:

html
<div wire:id="456">
    <h1>第一篇文章</h1>

    <p>文章内容</p>

    <button wire:click="$refresh">刷新文章</button>
</div>

这里需要注意的重要一点是,当在子组件上触发更新时,只有该组件的数据被发送到服务器,只有该组件被重新渲染。

现在让我们看看不太直观的场景:更新父组件。

更新父组件

作为提醒,以下是父 Posts 组件的 Blade 模板:

blade
<div>
    文章限制: <input type="number" wire:model.live="postLimit">

    @foreach ($posts as $post)
        <livewire:show-post :$post :wire:key="$post->id">
    @endforeach
</div>

如果用户将"文章限制"值从 2 更改为 1,将仅在父组件上触发更新。

以下是请求负载可能的样子:

js
{
    updates: { postLimit: 1 },

    snapshot: {
        memo: { name: 'posts', id: '123' },

        state: { postLimit: 2, ... },
    },
}

如您所见,只有父 Posts 组件的快照被发送到服务器。

您可能会问自己一个重要的问题:当父组件重新渲染并遇到子 show-post 组件时会发生什么?如果它们的快照没有包含在请求负载中,它将如何重新渲染子组件?

答案是:它们不会被重新渲染。

当 Livewire 渲染 Posts 组件时,它将为遇到的任何子组件渲染占位符。

以下是上述更新后 Posts 组件的渲染 HTML 可能的样子:

html
<div wire:id="123">
    文章限制: <input type="number" wire:model.live="postLimit">

    <div wire:id="456"></div>
</div>

如您所见,只渲染了一个子组件,因为 postLimit 被更新为 1。但是,您还会注意到,完整的子组件没有被渲染,只有一个带有匹配 wire:id 属性的空 <div></div>

当此 HTML 在前端接收时,Livewire 将把此组件的旧 HTML morph 为这个新 HTML,但会智能地跳过任何子组件占位符。

效果是,在 morphing 之后,父 Posts 组件的最终 DOM 内容将如下所示:

html
<div wire:id="123">
    文章限制: <input type="number" wire:model.live="postLimit">

    <div wire:id="456">
        <h1>第一篇文章</h1>

        <p>文章内容</p>

        <button wire:click="$refresh">刷新文章</button>
    </div>
</div>

性能影响

Livewire 的独立组件架构对您的应用程序可能有积极和消极的影响。

这种架构的一个优势是它允许您隔离应用程序中昂贵的部分。例如,您可以将慢速数据库查询隔离到其自己的独立组件中,其性能开销不会影响页面的其余部分。

然而,这种方法最大的缺点是,因为组件是完全分离的,组件间的通信/依赖变得更加困难。

例如,如果您有一个属性从上面的父 Posts 组件传递到嵌套的 ShowPost 组件,它不会是"响应式的"。因为每个组件都是独立的,如果对父组件的请求更改了传递给 ShowPost 的属性值,它不会在 ShowPost 内部更新。

Livewire 已经克服了许多这些障碍,并为这些场景提供了专用的 API,如:响应式属性可建模组件$parent 对象

有了关于嵌套 Livewire 组件如何运作的这些知识,您将能够做出更明智的决定,关于何时以及如何在应用程序中嵌套组件。