为何将FRP实现改为更符合响应式的写法后,threepenny-gui贪吃蛇出现卡顿?
问题原因
1. 先定义managerB无渲染的原因
Threepenny-gui 中 UI.tick 返回的事件流,只有在定时器实例完成初始化、正式启动后才会激活。你通过 RecursiveDo 提前绑定事件构造 managerB 时,timer 还未完成注册,事件源处于未激活状态,后续 timer 启动后之前的订阅也不会生效,最终 accumB 拿到的是空事件流,状态永远停留在初始值,依赖该状态的渲染逻辑自然不会输出内容。
2. managerB 后置出现卡顿抖动的原因
- 首次卡顿:RecursiveDo 块中状态 Behavior、DOM 事件、定时器事件的依赖形成了延迟求值的 thunk 链,首次触发方向键时需要一次性完成整条依赖链的求值,包括事件合并、状态更新计算、渲染绑定的初始化,所以会出现阻塞。
- 后续抖动:定时器事件和键盘事件的更新顺序没有被显式指定,Threepenny 的事件轮询在同一帧内如果同时收到 tick 事件和键盘事件,状态更新的顺序不固定,会导致状态跳步、重复计算,表现为渲染抖动。
FRP 编写惯用结构
在 Threepenny-gui 中写纯事件驱动的 FRP 逻辑,遵循以下顺序即可避免上述问题:
- 优先初始化所有事件源:不要依赖 RecursiveDo 获取未初始化的事件源,先完成定时器、DOM 事件等所有事件源的创建注册
-- 第一步:初始化所有事件源 timer <- UI.timer 100 -- 指定帧间隔创建并初始化定时器 keyDownE <- UI.keydown UI.window -- 注册全局键盘按下事件
- 显式转换并合并所有状态更新事件:把每个事件源都转成状态修改函数的 Event,同时用
unionWith显式指定多事件同时触发时的更新优先级,避免隐式依赖事件触发顺序
-- 第二步:统一构造所有状态更新事件 let tickUpdateE = UI.tick timer $> \manager -> updateByTick manager keyUpdateE = keyDownE <&> \key manager -> updateByKey key manager -- 显式指定合并策略:同一帧下先处理键盘输入再处理帧更新 allUpdatesE = unionWith (flip (.)) tickUpdateE keyUpdateE
- 最后构造状态 Behavior:用合并后的统一更新事件构造
accumB,再绑定渲染逻辑
-- 第三步:构造状态Behavior并绑定渲染 managerB <- accumB initialManager allUpdatesE sink UI.render $ managerB <&> renderManager
你最初用 newEvent 做中转的写法本身是符合 Threepenny 执行模型的:newEvent 创建的事件源是立即激活的,后续任何时候调用 addUpdate 都能被 accumB 正常接收,不会有初始化顺序问题,只是写法上不如纯事件定义简洁。如果要追求符合FRP规范的写法,严格按照「事件源初始化→事件合并→状态构造→渲染绑定」的顺序编写即可,不要用 RecursiveDo 绕事件源的初始化顺序。
内容的提问来源于stack exchange,提问作者brandonchinn178
相关产品推荐
相关产品推荐

