Elmish.WPF嵌套子模型是否支持异步单次刷新与并行运行
问题1:子模型使用Cmd.OfAsync.either能否实现全量更新完成后仅触发一次UI刷新?
不能。
Elmish.WPF的UI触发规则非常明确:每执行完一次update函数、返回新的Model实例,绑定层就会执行一轮变更检测,同步触发对应UI更新。如果在Row/Column/Cell每个子模型层单独使用Cmd.OfAsync.either,每个异步任务完成时都会独立派发Msg、独立触发一次完整的update流程,最终会导致多轮次的UI刷新,反而会增加额外开销,完全达不到单次延迟刷新的效果。
要实现全量更新后单次刷新,必须把所有更新逻辑收拢到顶层(Appointments模块层级即可)的单个异步命令中:
- 不要在各层子模型中分散定义异步Cmd
- 顶层异步任务等待所有子数据计算、组装完成,生成完整的新嵌套模型结构后,只派发一次「更新完成」的Msg
- 对应update分支一次性替换整个模型中变化的部分,整个update流程仅执行一次,绑定层仅做一轮变更检测,自然只会触发一次UI刷新。
Elmish.WPF本身默认会合并单次同步update过程中的所有变更,不会在更新中途触发UI,你当前遇到的多次刷新问题,本质就是多层子模型各自派发Cmd、触发了多个独立的update循环导致的。
问题2:能否实现所有子模型的异步并行更新?
完全可行,这也是这类嵌套列表场景的标准性能优化手段。
所有并行逻辑同样收拢在顶层异步命令中实现即可,不需要分散到子层。F#内置的Async.Parallel工作流可以直接把无依赖的子任务组合为并行执行的逻辑,示例代码如下:
let loadFullAppointmentData () = async { // 拉取最外层行基础数据 let! rawRows = fetchAllRowBaseData () // 并行处理每一行的列、单元格数据加载 let! processedRows = rawRows |> List.map (fun row -> async { // 拉取当前行关联的所有列基础数据 let! rawColumns = fetchColumnsByRowId row.Id // 并行处理当前行下每一列的单元格加载 let! processedColumns = rawColumns |> List.map (fun col -> async { let! cells = fetchCellsByRowAndColumnId row.Id col.ColumnId return { Column.Model.ColumnId = col.ColumnId; Cells = cells; SelectedCellId = None } }) |> Async.Parallel return { Row.Model.Id = row.Id Columns = processedColumns |> List.ofArray SelectedColumn = None } }) |> Async.Parallel return processedRows |> List.ofArray }
上述实现中,所有没有前后依赖的子数据拉取、子模型构造逻辑都会在线程池并行运行,等全部任务执行完成后才会返回完整的Rows列表,刚好和问题1提到的单次UI刷新逻辑适配,不会出现中间态的无效刷新。
额外性能提示
你当前使用的多层subModelSeq绑定本身自带按ID做增量比对的逻辑,只要每次更新时不全量新建所有子对象实例,仅替换实际发生数据变更的Cell/Column/Row实例,绑定层只会更新对应变化的UI元素,不会重绘整个DataGrid。如果并行计算完成后,能和旧Model做一次引用级别的diff,复用未发生变更的子实例,加载和渲染性能还能进一步提升。
内容的提问来源于stack exchange,提问作者Alan Wayne

