
简介这是一份面向.NET桌面开发者的Avalonia ReactiveUI MVVM实践Demo帮助读者在跨平台UI框架中运用响应式数据绑定与命令机制。资源共2000个文件、约223.6MB其中以dll库文件、xml配置、class编译产物、png界面资源为主要组成同时包含少量源代码与项目工程文件便于对照学习。已有1381人学习下载。Demo从MVVM三要素切入展示了如何定义Model、ViewModel与View的DataContext关联在ReactiveUI部分示例覆盖可观察属性ReactiveObject、ReactiveCommand命令、WhenAnyValue多属性变更通知、基于Rx的响应式UI布局以及ObserveOn/SubscribeOn错误处理等关键用法。通过分析App.xaml、MainWindow.xaml、MainViewModel.cs、Model.cs等典型文件读者可快速掌握在Avalonia中搭建响应式MVVM应用的完整链路并延伸到复杂业务场景下的数据流组织和界面更新策略。1. 项目介绍Avalonia与ReactiveUI的组合到底解决了什么问题先说结论Avalonia是一个跨平台的.NET UI框架ReactiveUI是一套基于响应式编程范式的MVVM框架两者配合起来可以让你用一套代码同时交付Windows、macOS、Linux甚至还能扩展到iOS和Android的UI层。我最早接触Avalonia是因为团队需要将一个WPF桌面应用移植到Linux环境当时调研了几个方案最后发现Avalonia的XAML语法、数据绑定、样式系统几乎就是WPF的翻版迁移成本极低而ReactiveUI又是我在WPF时代就一直在用的MVVM框架两者搭配属于顺理成章的选择。很多人会问Avalonia本身不强依赖MVVM框架为什么还要引入ReactiveUI答案很简单——普通MVVM能解决“分层”问题但解决不了“状态同步”问题。WPF或者Avalonia的原生数据绑定虽然支持INotifyPropertyChanged但当一个界面上有多个交互源输入框、按钮、下拉框、异步加载需要互相联动时手写事件、手动管理订阅、手动做线程切换代码很快就会失控。ReactiveUI在这方面的优势是它把属性、命令、异步操作全部统一成可观察对象Observable通过LINQ风格的运算符做数据流的变换与合并再加上内置的调度器Scheduler处理UI线程切换MVVM的复杂度可以从“状态同步”转移为“声明数据流”这恰恰是界面逻辑最核心的痛点。这篇文章适合谁读如果你在WPF里用过MVVM想了解Avalonia的用法或者你已经在用Avalonia但觉得目前的MVVM实现代码比较冗余又或者你想看看ReactiveUI这种响应式方案在实际项目里怎么落地这篇内容都可以做参考。我会直接从项目工程出发讲清楚核心设计、关键代码、踩过的坑尽量不写那种“只给概念不给代码”的教程。2. 技术选型与整体设计思路2.1 为什么不用普通MVVM框架而要选ReactiveUI很多Avalonia项目最开始的写法是这样的写一个ViewModel类继承一个基类属性setter里调用OnPropertyChanged命令用RelayCommand去包一个方法。这种写法本身没问题适用于小项目但一旦界面交互复杂起来问题就会逐步暴露属性之间有联动关系时你需要在setter里手动触发其他属性的PropertyChanged比如“用户名输入合法后登录按钮才能点击”你就得在UserName的setter里判断并通知LoginCommand的CanExecute变化这种依赖是隐式的日子久了没人能说清触发链。异步操作的生命周期管理很麻烦。登录按钮点击后要禁用按钮、显示加载中结束后恢复取消操作要防抖、要防止界面关闭后回调刷新UI手写这套逻辑容易漏。线程切换是每个异步MVVM开发者的痛。从后台线程取完数据要回到UI线程更新属性Code Behind里记得Dispatcher.BeginInvokeViewModel里就各有各的写法。ReactiveUI对这三个问题给出了统一的解法。它把属性声明为ObservableAsPropertyHelperOAPH把命令封装成ReactiveCommand把异步方法通过ReactiveCommand.CreateFromTask包装由框架自动处理CanExecute状态与执行状态的绑定同时调度器负责把数据流里产生的值投递到UI线程。我实际迁移过一个后台管理系统原来的ViewModel大约有3000行重构为ReactiveUI之后压缩到1200行以内最大的变化不是代码变少而是几乎不再需要手写“通知其他属性更新”这类代码数据间的关系通过可观察流表达出来链路清晰很多。2.2 项目结构与MVVM分层设计一个使用ReactiveUI的Avalonia项目我习惯按下面的结构组织src/ YourApp.sln YourApp/ App.axaml Program.cs ViewModels/ ViewModelBase.cs MainWindowViewModel.cs LoginViewModel.cs DashboardViewModel.cs Views/ MainWindow.axaml LoginView.axaml DashboardView.axaml Models/ User.cs Order.cs Services/ IApiClient.cs ApiClient.csView层只放Axaml和Code BehindCode Behind里不写业务逻辑只做两件事在构造函数里调用InitializeComponent()并获取ViewModel实例。调用WhenActivated()方法注册视图激活时的订阅逻辑。ViewModel层继承ViewModelBase它继承ReactiveUI的ReactiveObject然后所有属性、命令、异步逻辑都以ReactiveUI的方式组织。Model层是纯数据类负责承载业务数据不引用任何UI相关类型。Service层是接口实现负责HTTP请求、数据库访问、文件读写等外部依赖。这里有个细节为了可测试性ViewModel永远只依赖接口不做具体实例化依赖注入通过Avalonia的ServiceCollection在Program.cs里统一注册。我见过不少项目把HttpClient直接new在ViewModel里这种写法会让单元测试变得极其痛苦ReactiveUI本身非常适合单元测试不要在依赖注入上偷懒。2.3 从WPF迁移视角看两者差异如果你是从WPF过来的有几个关键差异要提前适应Avalonia的DataContext继承方式和WPF一致但它的绑定默认是DataType可选的不像WPF那样在IDE里容易感知类型错误所以建议所有Binding都写x:DataType既能在编译期检查绑定路径又能提升性能。Avalonia没有WPF里的Dispatcher它用的是Dispatcher.UIThread。在ReactiveUI中倒是不用太操心这个因为ReactiveCommand默认会在UI线程调度器中执行。Avalonia的样式系统直接看WPF的差不多但资源字典的key写法有差异这些不影响MVVM设计真遇上了查一下文档就行。3. ReactiveUI核心机制与在Avalonia里的正确用法3.1 属性通知ReactiveObject和ObservableAsPropertyHelperReactiveUI的基类是ReactiveObject它实现了INotifyPropertyChanged用法上酷似WPF的ViewModelBasepublic class LoginViewModel : ReactiveObject { private string _userName; public string UserName { get _userName; set this.RaiseAndSetIfChanged(ref _userName, value); } }RaiseAndSetIfChanged是ReactiveUI为了方便属性通知提供的封装它比较新旧值只有真的变化了才触发PropertyChanged同时返回是否发生了变更。这个写法看起来普通但它是后续所有响应式链路的地基。对于“由其他属性计算得到的属性”ReactiveUI推荐用ObservableAsPropertyHelperpublic class LoginViewModel : ReactiveObject { private readonly ObservableAsPropertyHelperbool _canLogin; public bool CanLogin _canLogin.Value; public LoginViewModel() { this.WhenAnyValue( x x.UserName, x x.Password, (name, pwd) !string.IsNullOrEmpty(name) !string.IsNullOrEmpty(pwd)) .ToProperty(this, x x.CanLogin, out _canLogin); } }这里的WhenAnyValue监听指定属性的变化把最新的值组合起来通过一个函数投影为bool值再通过ToProperty输出到CanLogin属性上。这就是响应式UI处理“属性联动”的核心手法你不需要手动在UserName的setter里通知CanLogin变化属性的关系在一处声明数据流自己会维护后续的一致性。我在实践中发现一个容易踩的坑ToProperty的第二个参数是表达式树x x.CanLogin它用于在属性变更时通知绑定。如果你把OAPH声明为private那绑定就收不到通知了必须至少是internal或public。另外OAPH的默认值取决于源流如果你希望在界面初始化时就有一个合理值可以在ToProperty前用.StartWith(value)指定初始值。3.2 ReactiveCommand与异步方法封装ReactiveCommand在ReactiveUI中替代了传统的RelayCommand它的核心特点是自带CanExecute流可以通过Execute的结果自动或手动控制命令可执行状态。支持异步CreateFromTask内部会处理任务的并发问题和UI线程调度。执行状态可以作为一个可观察对象被订阅方便写“加载中”这类交互状态。示例一个登录按钮的ReactiveCommandpublic ReactiveCommandUnit, Unit LoginCommand { get; } public LoginViewModel(IAuthService authService) { var canLogin this.WhenAnyValue( x x.UserName, x x.Password, (name, pwd) !string.IsNullOrWhiteSpace(name) !string.IsNullOrWhiteSpace(pwd)); LoginCommand ReactiveCommand.CreateFromTask( async () { await authService.LoginAsync(UserName, Password); }, canExecute: canLogin); LoginCommand.IsExecuting .ToProperty(this, x x.IsBusy, out _isBusy); }IsExecuting是ReactiveCommand自带的可观察流当命令体正在执行时为true完成后为false。我通常直接把它接到OAPH属性上这样XAML里就可以通过IsBusy控制加载动画的可见性不需要额外写任何“try/finally”来复位状态。在Avalonia里绑定命令很简单Button Content登录 Command{Binding LoginCommand} IsEnabled{Binding !CanLogin} /注意这里我用了IsEnabled绑定了一个取反的CanLogin。如果不用ReactiveCommand你可能需要在ViewModel里暴露两个属性而ReactiveCommand本身已经把CanExecute封装在命令内部理论上可以直接通过命令的CanExecute影响按钮可用性但Avalonia的Button在Command的CanExecute为false时是否自动禁用取决于Command.CanExecute的实现是否实现了ICanExecuteChanged等机制。实测Avalonia的ReactiveUI绑定可以正常工作但我个人习惯还是显式绑定IsEnabled因为WPF背景的人一眼就能看懂排查也方便。3.3 视图激活与生命周期管理WhenActivatedAvalonia的View和ReactiveUI结合使用时有一个非常实用的入口叫WhenActivated。它在View进入可视化树时触发在移出时自动释放订阅相当于一个精简版的“页面生命周期”。典型写法public partial class LoginView : ReactiveUserControlLoginViewModel { public LoginView() { InitializeComponent(); this.WhenActivated(disposables { this.OneWayBind(ViewModel, vm vm.IsBusy, v v.LoadingIndicator.IsVisible) .DisposeWith(disposables); this.BindCommand(ViewModel, vm vm.LoginCommand, v v.LoginButton) .DisposeWith(disposables); }); } }注意基类是ReactiveUserControlTViewModel而不是普通UserControl这样设计是为了在View内部通过ViewModel属性安全地拿到类型化的ViewModel实例。使用BindCommand绑定命令后按钮的点击会被框架自动处理不用在Code Behind里注册Click事件。DisposeWith(disposables)非常关键。ReactiveUI的绑定本质上是订阅数据流如果没有在View退出时释放这些订阅会发生内存泄漏。WhenActivated配合DisposeWith的模型让生命周期管理变得非常直观Enter时订阅Exit时释放。有一点要注意WhenActivated里的代码不会在构造函数中立刻执行它要等View真正被加载到VisualTree中。如果你在构造函数里尝试读取ViewModel的某个属性值那个值可能还没准备好不过ReactiveUI绑定机制本身不依赖这个时机所以正常业务代码不会遇到问题。3.4 路由与视图定位Routing大型客户端应用绕不开页面跳转。ReactiveUI提供了一个轻量级路由方案RoutingState和IScreen。先定义Screenpublic class AppViewModel : ReactiveObject, IScreen { public RoutingState Router { get; } public AppViewModel() { Router new RoutingState(); Router.Navigate.Execute(new LoginViewModel(this)); } }然后注册View的定位器。ReactiveUI有一个默认的ViewLocator它通过命名约定查找ViewViewModel名称为LoginViewModel它就找LoginView。在Avalonia里需要手动实现IViewLocator并注册到依赖注入容器public class ViewLocator : IViewLocator { public IViewFor ResolveViewT(T viewModel, string contract null) where T : class { var viewType Type.GetType($YourApp.Views.{viewModel.GetType().Name.Replace(ViewModel, View)}); if (viewType null) return null; return Activator.CreateInstance(viewType) as IViewFor; } }在View中通过Router控制导航Router.Navigate.Execute(new DashboardViewModel());我在实际项目里没有用ReactiveUI的Router做所有页面流转因为侧边栏内容区的主界面结构用Router并不顺手。我最终的方案是登录页用独立的窗口登录成功后把主窗口的DataContext切换为MainViewModel内部用Avalonia的TransitioningContentControl承载不同页面。ReactiveUI的路由适合层级简单、跳转明确的场景如果你的App是那种“首页—详情—设置”的线性流程直接用它如果是复杂的仪表盘自己控制内容切换反而更清晰。4. 完整实战从零搭建一个ReactiveUI风格的Avalonia项目4.1 创建工程与安装依赖我用的是.NET 8Avalonia版本是11.x。创建工程dotnet new install Avalonia.Templates dotnet new avalonia.mvvm -o YourApp cd YourApp dotnet add package ReactiveUI dotnet add package ReactiveUI.Fody dotnet add package Avalonia.ReactiveUI这里解释一下Avalonia.ReactiveUI为什么必须装Avalonia官方为ReactiveUI提供了一组集成扩展包比如让ReactiveWindowTViewModel、ReactiveUserControlTViewModel可以直接使用而且App.axaml.cs中的UseReactiveUI()扩展方法也在这个包里它会配置好ReactiveUI依赖的调度器等基础服务。ReactiveUI.Fody是可选的它通过IL织入weaving自动实现属性通知。装完之后属性setter可以直接写成[Reactive] public string UserName { get; set; }不需要手动调用RaiseAndSetIfChanged。这个特性在代码量大的项目里非常香但有个注意点Fody是在编译时改IL如果你在调试时看到属性的setter行为异常先检查有没有装Fody以及是否在csproj里配置了WeaverFiles包含ReactiveUI.Fody.xml。我早期踩过这个坑表现是属性赋值后UI不更新后来发现是Fody的织入没有生效白白排查了半小时。如果不喜欢Fody的“魔法”完全可以不用它手写RaiseAndSetIfChanged也很清晰。我现在的习惯是纯POCO属性用Fody的[Reactive]涉及OAPH属性的场景还是手写因为OAPH必须手动声明。4.2 准备一段可直接运行的示例代码为了让你有一个直观参照我写一个“登录后进入仪表盘”的极简示例。先看Model和Servicepublic class User { public string Name { get; set; } public string Token { get; set; } } public interface IAuthService { TaskUser LoginAsync(string userName, string password); } public class FakeAuthService : IAuthService { public async TaskUser LoginAsync(string userName, string password) { await Task.Delay(1500); // 模拟网络请求 if (userName admin password 123456) return new User { Name Admin, Token fake-token }; throw new Exception(用户名或密码错误); } }登录的ViewModelpublic class LoginViewModel : ReactiveObject { private readonly IAuthService _authService; private readonly IScreen _hostScreen; [Reactive] public string UserName { get; set; } [Reactive] public string Password { get; set; } [Reactive] public string ErrorMessage { get; set; } private readonly ObservableAsPropertyHelperbool _isBusy; public bool IsBusy _isBusy.Value; public ReactiveCommandUnit, Unit LoginCommand { get; } public LoginViewModel(IAuthService authService, IScreen hostScreen) { _authService authService; _hostScreen hostScreen; var canLogin this.WhenAnyValue( x x.UserName, x x.Password, (name, pwd) !string.IsNullOrWhiteSpace(name) !string.IsNullOrWhiteSpace(pwd)); LoginCommand ReactiveCommand.CreateFromTask( async () { ErrorMessage null; try { var user await _authService.LoginAsync(UserName, Password); // 登录成功后导航 _hostScreen.Router.Navigate.Execute(new DashboardViewModel(user)); } catch (Exception ex) { ErrorMessage ex.Message; } }, canExecute: canLogin); LoginCommand.IsExecuting .ToProperty(this, x x.IsBusy, out _isBusy); } }注意LoginCommand的异常处理。ReactiveCommand 默认会把异常包装后传给它的ThrownExceptions可观察流如果在订阅里没有处理异常可能会被吞掉或冒泡到全局。我这里的做法是直接在异步方法内部做try-catch把错误信息赋给ErrorMessage简单直接。更规范的做法是订阅LoginCommand.ThrownExceptions后续需要统一处理“网络错误重试”之类的逻辑再切换也不迟。View的Code Behindpublic partial class LoginView : ReactiveUserControlLoginViewModel { public LoginView() { InitializeComponent(); this.WhenActivated(disposables { this.Bind(ViewModel, vm vm.UserName, v v.UserNameTextBox.Text) .DisposeWith(disposables); this.Bind(ViewModel, vm vm.Password, v v.PasswordTextBox.Text) .DisposeWith(disposables); this.BindCommand(ViewModel, vm vm.LoginCommand, v v.LoginButton) .DisposeWith(disposables); this.OneWayBind(ViewModel, vm vm.ErrorMessage, v v.ErrorTextBlock.Text) .DisposeWith(disposables); this.OneWayBind(ViewModel, vm vm.IsBusy, v v.LoadingIndicator.IsVisible) .DisposeWith(disposables); }); } }如果你不喜欢Bind/OneWayBind这套API也可以直接在XAML里写{Binding UserName}然后在Code Behind里不写绑定代码。ReactiveUI在Avalonia里对原生的XAML绑定是兼容的只是使用Binding API的好处是能拿到类型检查、便于测试以及在WhenActivated中统一管理生命周期。我个人的建议是简单页面用XAML绑定就够复杂页面尤其是有多个组合绑定、需要响应式联动时用ReactiveUI的绑定API。4.3 依赖注入与启动流程在Program.cs里配置服务容器public static void Main(string[] args) { BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); } public static AppBuilder BuildAvaloniaApp() AppBuilder.ConfigureApp() .UsePlatformDetect() .UseReactiveUI() .LogToTrace();在App.axaml.cs里注册服务public class App : Application { public override void Initialize() { AvaloniaXamlLoader.Load(this); } public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { var services new ServiceCollection(); services.AddSingletonIAuthService, FakeAuthService(); services.AddSingletonIScreen, AppViewModel(); services.AddSingletonAppViewModel(); services.AddTransientLoginViewModel(); services.AddTransientDashboardViewModel(); services.AddSingletonIViewLocator, ViewLocator(); var provider services.BuildServiceProvider(); desktop.MainWindow new MainWindow { DataContext provider.GetRequiredServiceAppViewModel() }; } base.OnFrameworkInitializationCompleted(); } }MainWindow的XAML里只放一个TransitioningContentControl用来承载Router的内容或者手动切换的内容都可以。如果使用Router直接在MainWindow里绑定Router即可。这里有个很多人容易忽略的点IViewLocator是ReactiveUI内部用来把ViewModel解析为View的服务必须注册到容器里。如果你用的是ReactiveUI的Router但没有注册ViewLocator运行时会报“无法定位View”的异常。注册方式就是上面代码里的AddSingletonIViewLocator, ViewLocator()然后在之前的ViewLocator实现里做命名映射。4.4 与DynamicData结合处理列表增删与筛选在管理类系统中列表操作是最常见的功能。ReactiveUI本身不带集合操作的高级能力但它的兄弟库DynamicData可以配合使用。DynamicData的核心是一个SourceCacheTKey, TObject或SourceListT当你对这个源进行增删改时它会自动产生变化集ChangeSetReactiveUI可以把它转换成ReadOnlyObservableCollectionT供Avalonia的ItemsControl绑定。举例Dashboard页面有一个订单列表支持搜索过滤public class DashboardViewModel : ReactiveObject { private readonly SourceListOrder _orders new SourceListOrder(); private readonly ReadOnlyObservableCollectionOrder _filteredOrders; public ReadOnlyObservableCollectionOrder Orders _filteredOrders; [Reactive] public string SearchText { get; set; } public DashboardViewModel(User user) { var filter this.WhenAnyValue(x x.SearchText) .Throttle(TimeSpan.FromMilliseconds(300)) .Select(text (FuncOrder, bool)(order string.IsNullOrEmpty(text) || order.Id.Contains(text) || order.Name.Contains(text))); _orders.Connect() .Filter(filter) .ObserveOn(RxApp.MainThreadScheduler) .Bind(out _filteredOrders) .Subscribe(); } public void LoadOrders(IEnumerableOrder orders) { _orders.Edit(list { list.Clear(); list.AddRange(orders); }); } }这段代码的效果是每当你修改SearchText搜索框会做300毫秒的防抖然后自动重新过滤列表UI无需手动刷新。对比传统做法在setter里手动调用集合的Filter或重置ItemsSourceDynamicData方式的优势在于数据流逻辑是声明式的多个数据源合并筛选条件时尤其灵活。我实际项目的经验是不要在需求简单时为了用而用DynamicData普通的ObservableCollection加手动筛选代码更直观但一旦列表有“实时增量更新、分组、排序、多条件组合筛选”这些高阶需求DynamicData可以节省掉大量手写的集合操作代码。它有一个学习曲线但值得花时间掌握。5. 常见问题与排查技巧实录5.1 点击按钮无反应命令不执行这类问题我遇到时一般从三方面排查检查绑定路径是否正确。Avalonia的Binding默认是相对DataContext的在ReactiveUserControlT中DataContext是自动赋值的但如果你在某些地方手动改了DataContext绑定可能就断了。检查CanExecute是否返回false。ReactiveCommand在CanExecute为false时不会执行而且Avalonia的Button在这种情况下通常表现为可点击但没反应。调试技巧是在canLogin的投影函数里打一个断点或Debug.WriteLine确认数据流有没有在触发。检查是否忘记DisposeWith(disposables)。如果BindCommand的订阅没有被释放在某些时候会出现订阅丢失的问题特别是一个View实例被多次创建和销毁后命令可能被GC回收导致点击不响应这种情况在热重启或者导航返回时会发生。用内存诊断工具看订阅数量能快速确认。5.2 属性变化了但UI不刷新优先检查属性是不是没有被RaiseAndSetIfChanged或[Reactive]标记。使用Fody时如果某个属性所在的类没有继承ReactiveObjectFody的织入会提示错误或直接不生效。另外OAPH属性的setter一般不可写如果你在代码里尝试给OAPH赋值它会编译报错所以这种问题多出现在普通属性上。还有一个容易被忽略的值类型相等比较。如果你给一个int属性赋了同样的值RaiseAndSetIfChanged会认为没有变化不触发通知。在列表的某个字段更新时如果新旧值相同界面确实不会刷新这在某些业务下会让人怀疑出Bug需要理解这是“有意行为”。5.3 跨线程更新界面导致的异常在ReactiveUI里如果你通过Observable的工厂方法创建了在后台线程上发射数据的流并且直接ToProperty到OAPH属性上ReactiveUI默认会把值调度到UI线程吗不一定。ToProperty的默认调度器是RxApp.MainThreadScheduler但这取决于你的MainThreadScheduler配置是否成功。在使用Avalonia.ReactiveUI且调用了UseReactiveUI()后RxApp.MainThreadScheduler会被设置为Avalonia的UI线程调度器所以正常情况下没问题。但我建议在写数据流时保持显式习惯Observable.Start(() ComputeHeavy(), RxApp.TaskpoolScheduler) .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, x x.Result, out _result);这样做的好处是即使以后更换宿主环境比如单元测试代码依然是自解释的。排查线程问题时检查有没有哪个流缺少ObserveOn是最高效的路径。5.4 ViewLocator返回null导致导航白屏Router导航后界面空白绝大多数原因是ViewLocator没匹配上类型名。先确认ViewModel的命名空间和View的命名空间默认约定是YourApp.ViewModels.FooViewModel→YourApp.Views.FooView如果你的项目是ViewModels和Views平级那么ViewLocator里的Type.GetType字符串需要完整命名空间。另外如果View没有继承ReactiveUserControlTIViewFor接口会无法解析也会导致白屏。快速验证方法在ResolveView里加日志打印出查找的类型名和结果Console.WriteLine($Try resolve: {viewModel.GetType().FullName});这样能一眼看出是命名约定问题还是View类型没实现接口的问题。6. 性能优化与项目级经验ReactiveUI在一套数据流上天然比较“IO密集”属性的每次变化都可能触发多个订阅链上的投影计算。性能优化有两个最基本的原则不要在小而频繁变化的数据上使用过重的组合流。比如一个滑块的值绑定每个滚动帧都会触发流计算如果流上还包含了Throttle、同步到集合、调接口那体验就毁了。滑块这种场景只做即时UI绑定不要参合业务逻辑。合理使用Throttle/DistinctUntilChanged。搜索框输入、下拉筛选这类场景Throttle能减少无效计算DistinctUntilChanged能过滤掉重复值触发的事件。这两个运算符是响应式UI代码中出镜率最高的调优工具。在内存方面前面已经提到过WhenActivatedDisposeWith的重要性再补一句对于长驻的订阅如全局事件总线即使View退出了也不应该释放这时不要放入disposables里而是用单独的CompositeDisposable做全局管理。还有一个Avalonia独有的优化点如果你的列表数据量很大Avalonia的ItemsControl默认是不做UI虚拟化的这和WPF的默认行为不同。需要手动在XAML中使用VirtualizingStackPanel或者开启延迟滚动优化。这个优化虽然和ReactiveUI无关但在ReactiveUI绑定的动态集合场景下很容易遇到数据源更新一次整个列表全部重新生成大量对象频繁重建。处理方式是确保ItemsControl没有包裹在不固定高度的容器里并且显式设置VirtualizingPanel.IsVirtualizingTrue。最后分享一个我在项目里常用的调试工具组合ReactiveUI自带一个RxApp.DefaultExceptionHandler可以在App.axaml.cs里设置一个全局异常处理把异常打到输出窗口和日志文件。我通常在开发环境这么配RxApp.DefaultExceptionHandler Observer.CreateException(ex { Debug.WriteLine($ReactiveUI exception: {ex}); });这样数据流里未捕获的异常就不会沉默消失。生产环境则可以接入正式的日志系统。这个配置对排查线上问题帮助极大尤其是用户反馈“界面没反应”的时候大多数情况下是某个异步流抛了异常而日志里恰好有这个记录。响应式编程的曲线确实比其他MVVM方式陡峭但一旦你习惯了用数据流来思考界面逻辑你会发现自己写的代码越来越“干净”界面是数据流的眼镜业务是数据流的处理器而ReactiveUI就是那根把两者接起来的管道。在Avalonia项目里这套组合带给我的体验和当年从Code Behind转向MVVM一样是一次彻底的思维升级。本文还有配套的精品资源点击获取