You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor渲染带独立生命周期对象列表时如何实现状态过渡动画

问题根因

你遇到的动画串位问题,本质是Blazor渲染列表时默认按组件在循环中的位置匹配复用实例,不会自动和绑定的ToastData对象做唯一关联。当前面的列表项被移除后,后面的组件实例会被错位复用、绑定到新的Data参数上,直接导致动画状态张冠李戴。

可直接落地的修复方案

1. 给循环项加@key,强制组件和数据一一对应

这是解决复用错位最核心的一步。Blazor识别到@key后,就不会按位置复用组件,而是保证同一个ToastData对象永远对应同一个Toast组件实例,从根源上避免动画状态串到其他提示上。
修改Toasts组件代码:

@inject ToastService ToastService
@implements IDisposable

<div class="toasts">
    @foreach (ToastData data in ToastService.List)
    {
         <!-- 绑定@key,强制实例与数据一一绑定 -->
         <Toast @key="data" Data="data" />
    }
</div>

@code {
    protected override void OnInitialized()
    {
        ToastService.OnListChange += RefreshList;
    }

    private void RefreshList()
    {
        // Timer事件运行在后台线程,必须用InvokeAsync包裹UI更新
        InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        // 组件销毁时注销事件,避免内存泄漏
        ToastService.OnListChange -= RefreshList;
    }
}

2. 拆分移除流程:先播动画,再删数据

不要在点击关闭/倒计时结束时直接把Toast从列表删除,先标记「待移除」状态触发离场动画,等动画播放完成后再真正从列表移除数据。
首先调整ToastData,补充状态标识:

public class ToastData
{
    private readonly ToastService _toastService;
    private bool _isCountdownStarted;
    private System.Timers.Timer _countdown;
    // 标记是否正在播放离场动画
    public bool IsHiding { get; private set; }

    public ToastData(ToastService toastService)
    {
        _toastService= toastService;
    }

    public void StartCountDown()
    {
        if (_isCountdownStarted || IsHiding) return;

        _countdown = new System.Timers.Timer(5000);
        _countdown.AutoReset = false;
        _countdown.Elapsed += TriggerHide;
        _countdown.Start();
        _isCountdownStarted = true;
    }

    // 第一步:触发离场动画,不删除数据
    public void TriggerHide()
    {
        if (IsHiding) return;
        _countdown?.Stop();
        _countdown?.Dispose();
        IsHiding = true;
        _toastService.NotifyStateChanged();
    }

    // 第二步:动画播放完成后,真正从列表移除
    public void RemoveAfterAnimation()
    {
        _toastService.RemoveToast(this);
    }

    private void TriggerHide(object source, ElapsedEventArgs args)
    {
        TriggerHide();
    }
}

然后调整ToastService,移除List getter里的副作用逻辑(原逻辑每次渲染都会执行StartCountDown,极易引发异常):

public class ToastService
{
    private readonly List<ToastData> _list = new();
    public IReadOnlyList<ToastData> List => _list.Take(5).ToList().AsReadOnly();
    public event Action OnListChange;

    // 仅通知UI刷新,不做增删操作
    public void NotifyStateChanged()
    {
        OnListChange?.Invoke();
    }

    public Task CreateToast(ToastData data)
    {
        _list.Add(data);
        // 新增Toast时统一启动倒计时,不要在List getter里启动
        data.StartCountDown();
        OnListChange?.Invoke();
        return Task.CompletedTask;
    }

    public Task RemoveToast(ToastData data)
    {
        if (_list.Remove(data))
        {
            OnListChange?.Invoke();
        }
        return Task.CompletedTask;
    }
}

3. Toast组件内处理动画生命周期

组件根据IsHiding状态添加离场动画CSS类,等动画时长走完后再通知服务删除数据。

<div @onclick="Close" class="toast @(Data.IsHiding ? "toast-hide" : "")">
    Some message on a toast
</div>

@code {
    [Parameter] public ToastData Data { get; set; }
    // 时长和CSS动画保持一致,单位毫秒
    private const int HideAnimationDuration = 300;

    protected override void OnParametersSet()
    {
        base.OnParametersSet();
        if (Data.IsHiding)
        {
            _ = WaitAnimationAndRemove();
        }
    }

    private async Task WaitAnimationAndRemove()
    {
        // 等待离场动画播放完成
        await Task.Delay(HideAnimationDuration);
        Data.RemoveAfterAnimation();
    }

    private void Close()
    {
        Data.TriggerHide();
    }
}

配套CSS参考:

.toast {
    padding: 12px 16px;
    margin: 8px 0;
    border-radius: 4px;
    background: #333;
    color: #fff;
    transition: all 0.3s ease;
    transform: translateX(0);
    opacity: 1;
}
.toast-hide {
    transform: translateX(120%);
    opacity: 0;
}

额外避坑点
  • 不要在属性getter中写StartCountDown这类带业务副作用的逻辑,getter会在每次组件渲染时被调用,很容易导致逻辑重复执行、倒计时异常。
  • 如果需要入场动画,直接在Toast组件OnAfterRender生命周期里加入场状态标识,延迟几十毫秒切换状态触发CSS过渡即可,逻辑和离场动画完全一致。
  • 如果后续需要支持Toast排序、插入新项到顶部等场景,@key依然能保证组件不会错位复用,不需要额外调整逻辑。

内容的提问来源于stack exchange,提问作者NetDev747

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 07:42:18