计算属性
计算属性是在 Livewire 中创建「派生」属性的方式。类似于 Eloquent 模型上的访问器,计算属性让你访问值,并在本次请求中将其 memoize(记忆化),供后续访问复用。
计算属性与组件的公共属性搭配使用时尤其有用。
基本用法
要创建计算属性,可在 Livewire 组件的任意方法上方添加 #[Computed] Attribute。一旦方法加上该 Attribute,就可以像访问其他属性一样访问它。
WARNING
请确保导入 Attribute 类
请确保导入所用的 Attribute 类。例如,下面的 #[Computed] Attribute 需要导入:use Livewire\Attributes\Computed;。
例如,下面的 show-user 组件使用名为 user() 的计算属性,根据名为 $userId 的属性访问 User Eloquent 模型:
<?php // resources/views/components/⚡show-user.blade.php
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\User;
new class extends Component {
public $userId;
#[Computed]
public function user()
{
return User::find($this->userId);
}
public function follow()
{
Auth::user()->follow($this->user);
}
};<div>
<h1>{{ $this->user->name }}</h1>
<span>{{ $this->user->email }}</span>
<button wire:click="follow">Follow</button>
</div>由于 user() 方法已添加 #[Computed] Attribute,该值可在组件的其他方法以及 Blade 模板中访问。
INFO
模板中必须使用 $this
与普通属性不同,计算属性不能直接在组件模板中使用。你必须通过 $this 对象访问它们。例如,名为 posts() 的计算属性必须在模板中通过 $this->posts 访问。
性能优势
你可能会问:为什么要使用计算属性?为什么不直接调用方法?
把方法当作计算属性访问,比直接调用方法更有性能优势。在内部,计算属性首次执行时,Livewire 会 memoize 返回值。这样,同一次请求中的后续访问会返回已记忆的值,而不会多次执行。
这样你可以自由访问派生值,而不必担心性能影响。
WARNING
计算属性仅在单次请求中 memoize
一个常见误解是:Livewire 会在页面上 Livewire 组件的整个生命周期内 memoize 计算属性。实际上并非如此。Livewire 只在单次组件请求期间 memoize 结果(不会跨请求持久化)。这意味着,如果计算属性方法包含昂贵的数据库查询,每次 Livewire 组件执行更新时都会再次执行该查询。
清除 memo
考虑下面这个有问题的场景:
- 你访问了一个依赖某属性或数据库状态的计算属性
- 底层属性或数据库状态发生变化
- 该属性的 memoize 值变得过时,需要重新计算
要清除或「作废」已存储的 memo,可以使用 PHP 的 unset() 函数。
下面是一个名为 createPost() 的操作示例:在应用中创建新帖子后,会使 posts() 计算属性过时——也就是说,需要重新计算 posts(),以包含新添加的帖子:
<?php // resources/views/components/⚡show-posts.blade.php
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
new class extends Component {
public function createPost()
{
if ($this->posts->count() > 10) {
throw new \Exception('Maximum post count exceeded');
}
Auth::user()->posts()->create(...);
unset($this->posts); // [tl! highlight]
}
#[Computed]
public function posts()
{
return Auth::user()->posts;
}
// ...
};在上面的组件中,由于 createPost() 在创建新帖子之前就访问了 $this->posts,计算属性会在创建新帖子之前被 memoize。为确保在视图中访问时 $this->posts 包含最新内容,需用 unset($this->posts) 清除 memo。
跨请求缓存
TIP
Memoization 与 Caching
到目前为止讨论的 memoization 只持续单次请求。若需要值在多次请求间持久保存,则需要真正的 Laravel 缓存。
有时你希望在 Livewire 组件的生命周期内缓存计算属性的值,而不是每次请求后都清除。这时可以使用 Laravel 的缓存工具。
下面是一个名为 userName() 的计算属性示例:我们不直接执行 Eloquent 查询,而是用 Cache::remember() 包裹查询,确保后续请求从 Laravel 缓存中取名字,而不是重新执行查询:
<?php // resources/views/components/⚡show-user.blade.php
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\User;
new class extends Component {
public $userId;
#[Computed]
public function userName()
{
$key = 'user-name'.$this->getId();
$seconds = 3600; // 1 hour...
return Cache::remember($key, $seconds, function () {
return User::find($this->userId)->name;
});
}
// ...
};由于每个 Livewire 组件实例都有唯一 ID,我们可以用 $this->getId() 生成唯一的缓存键,该键仅适用于同一组件实例的后续请求。
不过你可能已经注意到,这段代码大多是可预期的,很容易抽象。因此,Livewire 的 #[Computed] Attribute 提供了方便的 persist 参数。给方法加上 #[Computed(persist: true)],无需额外代码即可达到同样效果:
use Livewire\Attributes\Computed;
use App\Models\User;
#[Computed(persist: true)]
public function userName()
{
return User::find($this->userId)->name;
}在上面的示例中,从组件访问 $this->userName 时,它会在页面上该 Livewire 组件存续期间持续被缓存。这意味着实际的 Eloquent 查询只会执行一次。
Livewire 会将持久化值缓存 3600 秒(一小时)。你可以通过向 #[Computed] Attribute 额外传入 seconds 参数来覆盖该默认值:
#[Computed(persist: true, seconds: 7200)]WARNING
在 Laravel 13 上缓存对象
新建的 Laravel 13 应用只会从缓存反序列化明确允许的 PHP 类。当 Laravel 无法还原持久化或缓存计算属性返回的对象时,Livewire 会重新求值该属性,以保证值仍然正确。在调试模式下,Livewire 还会向应用日志写入警告,因为该值并非从缓存提供。
优先缓存标量或数组。若要有意缓存对象,请将其对象图中的每个类都加入 config/cache.php 的 cache.serializable_classes:
'serializable_classes' => [
App\Models\User::class,
],TIP
调用 unset() 会同时清除 memo 与缓存
如前所述,你可以用 PHP 的 unset() 方法清除计算属性的 memo。这对使用 persist: true 参数的计算属性同样适用。对持久化计算属性调用 unset() 时,Livewire 不仅会清除请求内的 memo,还会清除 Laravel 缓存中的底层缓存值。
跨所有组件缓存
除了在单个组件生命周期内缓存计算属性的值,你还可以使用 #[Computed] Attribute 提供的 cache: true 参数,在应用的所有组件间缓存计算属性的值:
use Livewire\Attributes\Computed;
use App\Models\Post;
#[Computed(cache: true)]
public function postTitles()
{
return Post::query()->pluck('title', 'id')->all();
}在上面的示例中,在缓存过期或被作废之前,应用中该组件的每个实例都会共享 $this->postTitles 的同一缓存值。
若需要手动清除计算属性的缓存,可以用 key 参数设置自定义缓存键:
use Livewire\Attributes\Computed;
use App\Models\Post;
#[Computed(cache: true, key: 'homepage-posts')]
public function postTitles()
{
return Post::query()->pluck('title', 'id')->all();
}何时使用计算属性?
除了性能优势外,还有一些其他场景下计算属性也很有帮助。
具体来说,在向组件的 Blade 模板传递数据时,有些场合下计算属性是更好的选择。下面是一个简单组件的 render() 方法,向 Blade 模板传递 posts 集合的示例:
public function render()
{
return view('livewire.show-posts', [
'posts' => Post::all(),
]);
}<div>
@foreach ($posts as $post)
<div wire:key="{{ $post->id }}">
<!-- ... -->
</div>
@endforeach
</div>虽然这对许多用例已经足够,但以下三种场景下,计算属性会是更好的替代方案:
有条件地访问值
若你在 Blade 模板中有条件地访问检索成本很高的值,可以用计算属性降低性能开销。
考虑下面这个未使用计算属性的模板:
<div>
@if (Auth::user()->can_see_posts)
@foreach ($posts as $post)
<div wire:key="{{ $post->id }}">
<!-- ... -->
</div>
@endforeach
@endif
</div>若用户无权查看帖子,检索帖子的数据库查询却已经执行了,而这些帖子在模板中从未被使用。
下面是使用计算属性改写上述场景的版本:
use Livewire\Attributes\Computed;
use App\Models\Post;
#[Computed]
public function posts()
{
return Post::all();
}
public function render()
{
return view('livewire.show-posts');
}<div>
@if (Auth::user()->can_see_posts)
@foreach ($this->posts as $post)
<div wire:key="{{ $post->id }}">
<!-- ... -->
</div>
@endforeach
@endif
</div>现在,由于我们通过计算属性向模板提供帖子,只有在真正需要数据时才会执行数据库查询。
使用内联模板
另一个适合使用计算属性的场景是在组件中使用内联模板。
下面是一个内联组件示例:由于我们在 render() 中直接返回模板字符串,没有机会向视图传递数据:
<?php // resources/views/components/⚡show-posts.blade.php
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;
new class extends Component {
#[Computed]
public function posts()
{
return Post::all();
}
public function render()
{
return <<<HTML
<div>
@foreach ($this->posts as $post)
<div wire:key="{{ $post->id }}">
<!-- ... -->
</div>
@endforeach
</div>
HTML;
}
};在上面的示例中,如果没有计算属性,我们将无法显式向 Blade 模板传递数据。
省略 render 方法
在 Livewire 中,另一种减少组件样板代码的方式是完全省略 render() 方法。省略后,Livewire 会使用自带的 render() 方法,按约定返回对应的 Blade 视图。
在这种情况下,你显然没有可用来向 Blade 视图传递数据的 render() 方法。
与其把 render() 方法重新加回组件,不如通过计算属性向视图提供这些数据:
<?php // resources/views/components/⚡show-posts.blade.php
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;
new class extends Component {
#[Computed]
public function posts()
{
return Post::all();
}
};<div>
@foreach ($this->posts as $post)
<div wire:key="{{ $post->id }}">
<!-- ... -->
</div>
@endforeach
</div>替代方案:Session 属性
若需要在页面刷新之间持久保存简单值,又不想做跨请求缓存,可考虑使用 #[Session] Attribute,而不是计算属性。
Session 属性适用于以下情况:
- 你希望用户专属值在页面重载后仍然保留(例如搜索过滤条件或 UI 偏好)
- 你不需要通过 URL 分享该值
- 该值简单,存储成本不高
例如,将搜索词存入 session:
use Livewire\Attributes\Session;
#[Session]
public $search = '';这样可在页面刷新后保留搜索值,而无需使用 URL 参数或计算属性缓存。
另见
- Properties — 了解基本属性管理
- Islands — 用懒计算值优化性能
- Computed Attribute — 使用 #[Computed] 做 memoization
- Components — 在视图中访问计算属性