You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何将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 逻辑,遵循以下顺序即可避免上述问题:

  1. 优先初始化所有事件源:不要依赖 RecursiveDo 获取未初始化的事件源,先完成定时器、DOM 事件等所有事件源的创建注册
-- 第一步:初始化所有事件源
timer <- UI.timer 100 -- 指定帧间隔创建并初始化定时器
keyDownE <- UI.keydown UI.window -- 注册全局键盘按下事件
  1. 显式转换并合并所有状态更新事件:把每个事件源都转成状态修改函数的 Event,同时用 unionWith 显式指定多事件同时触发时的更新优先级,避免隐式依赖事件触发顺序
-- 第二步:统一构造所有状态更新事件
let tickUpdateE = UI.tick timer $> \manager -> updateByTick manager
    keyUpdateE = keyDownE <&> \key manager -> updateByKey key manager
    -- 显式指定合并策略:同一帧下先处理键盘输入再处理帧更新
    allUpdatesE = unionWith (flip (.)) tickUpdateE keyUpdateE
  1. 最后构造状态 Behavior:用合并后的统一更新事件构造 accumB,再绑定渲染逻辑
-- 第三步:构造状态Behavior并绑定渲染
managerB <- accumB initialManager allUpdatesE
sink UI.render $ managerB <&> renderManager

你最初用 newEvent 做中转的写法本身是符合 Threepenny 执行模型的:newEvent 创建的事件源是立即激活的,后续任何时候调用 addUpdate 都能被 accumB 正常接收,不会有初始化顺序问题,只是写法上不如纯事件定义简洁。如果要追求符合FRP规范的写法,严格按照「事件源初始化→事件合并→状态构造→渲染绑定」的顺序编写即可,不要用 RecursiveDo 绕事件源的初始化顺序。


内容的提问来源于stack exchange,提问作者brandonchinn178

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 17:06:00