За замовчуванням дії одного компонента виконуються по черзі: поки йде запит, наступні чекають. Асинхронна дія виконується одразу, паралельно з іншими, - не чекає черги й нікого не блокує.
use Livewire\Attributes\Async;
new class extends Component {
#[Async]
public function trackClick(string $url): void
{
Analytics::track('external-link', ['url' => $url, 'user' => auth()->id()]);
}
};
<a href="{{ $url }}" target="_blank" wire:click.async="trackClick('{{ $url }}')">Перейти</a>
Або точково в шаблоні: wire:click.async, wire:intersect.async.
Чому стан і асинхронність несумісні. Кожен запит стартує зі знімка стану, який браузер мав на момент відправки. Паралельні запити не бачать результатів одне одного:
#[Async] // так не можна
public function increment(): void
{
$this->count++;
}
П'ять швидких кліків - п'ять запитів, кожен починає з count = 0, кожен повертає 1. Лічильник показує 1 замість 5. Зміни губляться, а остаточний стан визначає відповідь, що прийшла останньою, - не обов'язково остання за часом дія.
Безпечні сценарії:
- чисті побічні ефекти: аналітика, журнали, постановка джоби в чергу, сповіщення - дія не змінює властивостей, що відображаються;
- дані для JavaScript: результат повертається в Alpine і зберігається там, а не в стані компонента:
<div x-data="{ suggestions: [] }">
<input x-on:input.debounce="suggestions = await $wire.fetchSuggestions($event.target.value)">
</div>
Правило: якщо асинхронна дія присвоює значення публічній властивості, що впливає на шаблон, - це помилка дизайну.
Захист від гонитви на сервері асинхронність не скасовує: дві паралельні дії, що змінюють один запис у базі, потребують атомарних операцій (increment()), транзакцій чи блокувань, як і будь-які паралельні HTTP-запити.
Зв'язок з іншими механізмами:
- з
#[Renderless]- класичне поєднання для «запустив і забув»; #[Isolate]- про групування між компонентами, а#[Async]- про чергу всередині компонента;- острівці теж ходять паралельно, і застереження про спільний стан для них те саме.