在Elm 0.19.1的update函数中,如何为带多参数的Msg正确使用Cmd.map?
首先得指出你当前做法的核心问题:把Multiselect.Model放进Msg里是违背Elm架构设计原则的。Msg的职责是描述「发生了什么事件」,而不是携带当前的状态数据——当你把模型塞进Msg后,异步命令(比如组件内部的延迟事件、DOM交互回调)触发时,这个Msg里的模型很可能已经是过时的了,这就是为什么你删除项后,下一个事件会「回滚」更新,因为它用的是旧模型计算新状态。
为什么最初的单参数版本能正常工作?
看你最初的代码:
type Msg = Yay Multiselect.Msg update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of Yay sub -> let ( subModel, subCmd, _ ) = Multiselect.update sub model.multiselectC -- 这里直接从当前最新的Model里取子模型 in ( { model | multiselectC = subModel }, Cmd.map Yay subCmd )
这里的关键是:处理Yay Msg时,你是从当前传入的最新Model中获取multiselectC子模型,再传给Multiselect.update计算新状态。Cmd.map只是把组件返回的子命令Msg包装成Yay,不需要额外携带模型——因为下一次处理这个子命令时,我们依然会从当时的最新Model里取子模型。
如果你确实需要给Msg加额外参数怎么办?
如果你的需求是区分多个同类型的组件(比如页面上有两个Multiselect),那应该传递组件的标识(比如ID、索引),而不是整个模型。举个例子:
-- 用Int作为组件ID来区分不同的Multiselect type Msg = Yay Int Multiselect.Msg update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of Yay componentId subMsg -> -- 第一步:根据ID从当前Model中取出对应的子模型 currentSubModel = case componentId of 1 -> model.multiselectC1 2 -> model.multiselectC2 _ -> Multiselect.defaultModel -- 默认模型 -- 第二步:用最新子模型和子Msg计算新状态 ( updatedSubModel, subCmd, _ ) = Multiselect.update subMsg currentSubModel -- 第三步:把更新后的子模型放回父Model newModel = case componentId of 1 -> { model | multiselectC1 = updatedSubModel } 2 -> { model | multiselectC2 = updatedSubModel } _ -> model -- 第四步:包装子命令,保留组件ID ( newModel, Cmd.map (Yay componentId) subCmd )
这种方式下,Cmd.map只需要把组件返回的子Msg和固定的组件ID重新包装成Yay Msg,而状态始终从当前最新的父Model中获取,完全不会出现过时状态的问题。
回到你的问题:为什么多参数Msg会导致异常?
你修改后的代码中,把Multiselect.Model塞进了Yay Msg:
type Msg = Yay Multiselect.Msg Multiselect.Model
当组件返回异步命令(比如删除项后的DOM重绘回调),这个命令被包装成Yay cmd subModel,而这里的subModel是当时计算出的旧状态。等这个命令触发时,父Model可能已经更新过了,但你依然用Msg里的旧subModel去计算新状态,自然会覆盖掉之前的正确更新,看起来就像「什么都没发生」。
总结正确的做法
- 永远不要把状态(Model)放进Msg里,Msg只传递事件的必要上下文(比如组件ID、用户输入的值等)。
- 处理Msg时,始终从当前传入的最新Model中获取需要的子状态进行计算。
- 使用
Cmd.map时,只需要把子Msg包装成父Msg,不需要携带任何状态——状态会在处理Msg时从当时的Model中获取。
这样就能保证你的update函数始终基于最新状态计算,不会出现异步事件导致的状态回滚问题。
备注:内容来源于stack exchange,提问作者TBatist

