为何Razor组件中SayHello方法会覆盖Subscribe订阅行为?
SayHello方法与Rx订阅的冲突问题分析 我是Reactive Extensions(Rx)新手,搞不懂为什么Razor组件里的SayHello方法会和Subscribe订阅事件产生冲突,调试时出现了预期外的结果:在两台电脑上打开网站,点击按钮后,首先触发组件的SayHello方法,随后订阅处理程序执行了4次。我原本以为Subscribe会为两个组件实例分别设置message变量。
相关代码
Index.razor
@page "/" @using ClassLibrary1 @inject Class1 _x; @implements IDisposable <PageTitle>Index</PageTitle> <h1>Hello, world!</h1> Welcome to your new app. <p>@message</p> <button type="button" @onclick="SayHello" class="btn btn-primary">Say Hello</button> @code { string message = ""; private IDisposable? _subscription; void SayHello() { message = _x.SayHello(); } protected override Task OnInitializedAsync() { _subscription = _x.Notifier.Subscribe(x => { message = x; InvokeAsync(StateHasChanged); }); return base.OnInitializedAsync(); } public void Dispose() { _subscription?.Dispose(); } }
Class1
using System.Reactive.Subjects; namespace ClassLibrary1 { public class Class1 { private Subject<string> _notifier; public IObservable<string> Notifier { get { return _notifier; } } public Class1() { _notifier = new Subject<string>(); } public string SayHello() { _notifier.OnNext("someone called SayHello"); return "Hello"; } } }
问题原因
1. 订阅未取消导致累积
组件在OnInitializedAsync中订阅了_x.Notifier,但没有在组件销毁时取消订阅。Blazor组件在页面刷新、导航离开时会被销毁,但如果不手动调用订阅的Dispose(),Subject会保留旧的订阅者。每次重新加载页面,新的组件实例又会添加一个新订阅,最终导致Subject的订阅者数量越来越多——你看到的4次执行,就是累积了4个未取消的订阅。
2. 执行顺序导致的"覆盖"假象
点击按钮时,SayHello方法先执行message = _x.SayHello();,而_x.SayHello()内部调用了_notifier.OnNext("someone called SayHello"),这会立即触发所有订阅者的回调,把message改成"someone called SayHello"。所以你看到的最终结果是订阅回调的内容,而非SayHello的返回值,看起来像是"覆盖"了订阅事件,实际是代码执行顺序的问题:同步调用OnNext会立即触发所有订阅者,覆盖了之前的赋值。
3. 服务生命周期的影响
如果Class1是注册为**单例(Singleton)服务,所有客户端会共享同一个Class1实例和内部的Subject。这意味着一个客户端点击按钮,所有客户端的订阅者都会收到通知。如果你的预期是每个客户端独立处理,应该将Class1注册为作用域(Scoped)**服务(Blazor Server中每个SignalR连接对应一个Scoped实例)。
解决方法
- 取消订阅,避免累积:让组件实现
IDisposable接口,保存订阅返回的IDisposable实例,在组件销毁时调用Dispose()取消订阅,代码如上述Index.razor所示。 - 调整执行逻辑,避免冲突:如果希望
SayHello的返回值和订阅通知共存,可以拆分message为两个变量,或在订阅回调中添加判断逻辑。 - 配置正确的服务生命周期:在Program.cs中注册
Class1时,根据需求选择生命周期:// Scoped:每个客户端独立实例 builder.Services.AddScoped<Class1>(); // 或 Singleton:所有客户端共享实例(根据业务需求选择) // builder.Services.AddSingleton<Class1>();
内容的提问来源于stack exchange,提问作者Rod

