05-18 用 Algebraic Effect 改进 React.js 框架设计的探索(一) #1
tiye
started this conversation in
Show and tell
Replies: 1 comment
|
@copilot 语义漂移是啥 |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
这份文档整理这个仓库在前端框架方向上的一组实验经验。它不是要证明“React 可以被 Koka 重写”,也不是要给出一个已经定型的新框架,而是想回答一个更具体的问题:
如果把 Algebraic Effect 当成前端架构里的第一等工具,组件、事件、局部状态、异步流程、测试边界,会发生什么变化?
这份总结仍然从“组件怎么写”开始,因为组件仍然是这类框架最重要的入口。但和前几个版本相比,现在更值得总结的已经不只是 hook,而是“组件约束”本身:哪些地方继续像 React,哪些地方故意不照着 React 走。
1. 组件首先仍然应该是普通函数
这次探索里最不想丢掉的,是 React 最有价值的一点:组件大部分时候应该仍然像普通函数。
在这个仓库里,组件函数依旧主要做三件事:
modelvnode例如 Todo 面板的入口仍然是一个返回
vnode的函数:这里最关键的经验不是语法,而是约束:
如果一个 Effect 系统让组件写法比 React 组件更绕、更难定位职责,那通常不是进步。
2. 组件 API 现在更像“受约束的 React”
这个仓库现在有意识地把一部分复杂度下沉到了框架层:
node_class(...)text_class(...)button_class(...)text_input(...)这样业务组件不再反复写一大串:
Nil例如现在的 Todo 输入区更接近“受约束的 React props 写法”:
这里值得强调的不是“它长得像 JSX”,而是:
这和 React 很像,但比 React 更强调“框架可以更厚一些,换来业务层更薄”。
3. 组件的几种典型用法
这次实验最后比较稳定的组件用法,可以分成下面几类。
3.1 纯渲染组件
最简单的一层,仍然只是根据参数返回
vnode。这类组件不需要本地状态,也不需要特殊能力。适合承载:
这种组件的意义在于,它让框架保持“默认简单”。不是所有组件都该被 hook、listener、effect row 包起来。
3.2 带局部状态的组件
第二类组件需要临时 UI 状态,比如:
这一层现在的做法是:
component(scope, render)给组件声明稳定作用域use_state(...)、use_state_pair(...)例如任务项组件:
这里得到的经验很明确:
state/effect hooks可以继续走 React 那种“稳定顺序 + index”的思路所以这个仓库最后保留了两套 API:
use_state、use_state_pair、use_effectuse_named_state、use_named_effect这相当于把“React 风格简洁调用”和“Respo 风格显式路径”做了一层分层,而不是二选一。
3.3 带局部副作用的组件
组件除了局部状态,还会有局部副作用,例如:
这一层继续沿用 hook 语义,用
use_effect(...)表达:这里的经验是:
useEffect这个交互模式本身更好的方向通常是:保留交互习惯,重写能力边界。
3.4 发送全局 action 的组件
组件里的事件,并不都应该直接闭包修改状态。
这次实验后比较清晰的分层是:
例如 Todo 里的这些按钮:
这里的好处是:
这部分和 React 社区常见的 reducer / dispatch 思路是一致的,只是这里 action 可以继续进入带 effect 的 reducer 路径。
3.5 带局部 closure listener 的组件
另一些事件并不适合进全局 dispatch,例如:
这一层如果也强行塞进全局 action,会让临时 UI 状态的路径变得很重。
所以这里保留了 closure listener,但现在这部分已经从“三个独立 listener effect”收敛成了一个统一的注册能力:
register_listener<model,browser_event_effect>业务代码仍然通过:
on_named_click(...)on_named_input(...)on_named_enter(...)来声明 listener,但组件签名不再需要把
on_click/on_input/on_enter一项项展开。4. 为什么 listener 不沿用 hook 的 index 规则
这是这次探索里非常重要的一条结论。
直觉上,既然
use_state和use_effect可以用 index,那 listener 似乎也可以这样做。但实际写 UI 会发现,listener 的稳定性来源和 hook 不一样。原因很简单:
if/else条件分支里所以最终稳定下来的规则是:
scope + indexscope + event kind + semantic name对应 API 是:
这是一条这次探索里非常重要的经验:
不要把所有组件内机制都压成 React hook 那一种 index 规则。
state/effect hooks和event listeners的稳定性来源不同,应该区别对待。5. 事件注册现在是怎么工作的
listener 现在不是直接把 closure 塞进 DOM,而是走一套更适合当前框架结构的注册流程。
5.1 组件里声明 listener
业务组件里写的是:
这些调用会:
semantic_name调用register_listener(...)run_event_registry(...)去解释5.2
run_event_registry(...)收集 handlerrun_event_registry(...)会把 listener 注册请求收集成一张 registry:同时返回一个轻量
listener(kind, payload)给 VDOM。也就是说:
5.3 renderer 把 listener 变成
data-k-*渲染后会落成:
data-k-clickdata-k-inputdata-k-enter5.4 DOM bridge 做事件委托
浏览器宿主层只做很薄的一层:
data-k-*event_registry里找到对应 closure 或 fallback action reducer这和 React 一样,也使用 delegated event 的思路;但和 React 不同的是,这里不是沿 Fiber 树查 handler,而是通过稳定 payload 和当前 registry 做路由。
6. listener 的新约束:防重复和防语义漂移
listener 走 semantic id 以后,会有两个新的风险:
所以 framework 层现在增加了两类 guard:
它们在两个阶段生效:
并且会在
app.kk里统一打日志。这条设计的重点不是“绝不允许出错”,而是:
这也是这条路线和 React 的一个明显差异:
React 默认把很多事件稳定性问题交给框架内部规则和用户习惯;这里则更愿意把 listener identity 设计成显式约束,并在运行时给出诊断。
7. 组件签名里的 Effect Row 到底带来了什么
如果只看组件表面,这套写法很像 React。但真正的差异,在函数类型上。
例如现在一个视图函数会显式写出自己需要什么:
use_state_pairuse_effectregister_listener<model,browser_event_effect>div这意味着组件依赖不再主要靠约定,而是直接进入类型。
这件事有两个直接后果。
第一,组件的能力边界更清楚。
你一眼就能知道这个组件:
第二,组件外层可以逐步把实现细节收口。
比如目前视图层用了
todo_view_effect、lab_view_effect这种 alias,把底层 effect row 压成更短的签名。这样既保留了类型信息,又不至于让组件声明完全失控。这比 React 常见的“看实现才知道组件依赖了什么 hook、什么 context、什么 service”要透明得多。
8. Algebraic Effect 真正改进 React 设计的地方
如果说组件写法本身没有被彻底颠覆,那么 Algebraic Effect 的真正价值,主要体现在下面几层。
8.1 把能力从“隐式依赖”变成“类型里的依赖”
例如 workflow 里会直接声明:
这比 React 里常见的做法更清晰,因为在 React 里同样的依赖通常会散落在:
而这里函数签名本身就说明了它需要:时间、服务调用、日志、当前操作者身份。
8.2 把异步流程留在一条 reducer 风格的路径里
perform_sync(...)和send_reply(...)这类流程,本质上是前端里最容易散掉的部分。在很多 React 项目里,这类逻辑最后会被拆成:
then/catch或 async callback而在这里,它更像一条线性的状态转换函数:
这就是 Algebraic Effect 对 React 架构最有启发的一点:
不是把异步藏起来,而是把它保留在 typed reducer path 里。
8.3 把 Context 从 provider 树变成显式 ambient capability
像
current_operator这种上下文值,在 React 里通常会放进 Context。这里的做法是把它建模成
valeffect。这样得到的经验是:这比 Context 更强的一点,是测试时替换非常直接;更弱的一点,是生态习惯和可视化工具目前远不如 React 完整。
8.4 把测试变成“换 handler”,而不是“重写业务路径”
这也是这次探索里最实用的一点。
浏览器里,
app.kk会安装一套 handler:confirm_action对应浏览器 confirmaudit对应浏览器日志wait_ms对应浏览器侧模拟等待current_operator对应浏览器环境给的值测试里,则安装另一套 handler:
wait_ms不真的等待,只记录 delayfetch_lab_snapshot返回 mock 数据post_lab_reply直接回 mock 回复now_label返回稳定时间戳audit写入收集器这样测试覆盖的是同一条业务逻辑,而不是“测试专用版本”的逻辑。
这比 React 里常见的大量 mock service、fake timer、render wrapper、provider scaffold 更统一。
9. 组件、事件、状态三者的分工,最后应该怎么落
走到目前这个版本,比较清楚的分工是:
9.1 组件负责描述 UI 和声明依赖
组件负责:
组件不负责:
9.2 全局状态变化尽量走 action dispatch
适合全局 action 的场景:
这里更接近 React reducer 思想。
9.3 局部临时交互允许走 closure listener
适合局部 closure 的场景:
这里不应该为了“架构统一”而强行全局化。
9.4 hook 和 listener 要用两种不同的稳定性策略
这是这次设计里最值得单独记住的一条:
这条边界一旦混掉,条件渲染很快就会把事件系统搞乱。
10. 这条路线相对 React 的收益和代价
收益
代价
所以更准确的结论不是“Algebraic Effect 会取代 React”,而是:
Algebraic Effect 为 React 风格框架提供了一种更强的能力边界建模方式,而 listener identity、宿主集成、测试边界这些原本容易模糊的地方,也可以被更明确地设计出来。
11. 目前最值得保留的结论
如果把这次探索压缩成几条设计结论,我会保留下面这些。
use_state/use_effect可以继续保留 React 的 hook 使用习惯,但要加上稳定组件作用域。scope + event kind + semantic name。12. 如果继续往前做,可以重点看什么
如果继续沿这条路线探索,下一步最有价值的问题不是再造更多组件 API,而是继续压实边界:
item_scope/panel_scoperun_local_state(...)和run_event_registry(...)再包成更高层的组件 runnerrespo/core.kk也就是说,真正值得继续深入的,不是“把 React API 一比一搬进 Koka”,而是借 React 的组件心智模型,重新设计副作用、能力边界和宿主集成方式。
这正是这个仓库目前最有价值的实验结果。
All reactions