增加了一个视频来解释这篇文章的内容: http://weibo.com/1651843872/E...
更新了 Respo 到 0.4.x
, 移除了原先基于 init-state
update-state
mutate!
的代码,
原先的版本是模仿的 React, 在组件曾经管理状态, 但同时支持热替换过程组件状态的稳定,
现在的版本 state tree 需要手段管理, 同时和 Virtual DOM 的结构对应,
也就是说, 从原来的自动管理编程了手工管理, 实际上是变麻烦了.
那么, 为什么? 切入点从当时发的微博可以看到:
http://weibo.com/1651843872/E...
我觉得 Respo 的状态树的设计有点问题, 已有的方案是基于节点位置信息生成的, 也就是和 DOM 结构一一对应, 实际上并不需要那么深, 而且状态树也不应该依赖 DOM 结构的信息. 随着页面开发手动构建状态树应该是更准确的做法. 可能会啰嗦, 但是会更精准.
http://weibo.com/1651843872/E...
组件状态这个事情, 按照 MVC 的套路, Model 是放到全局和 View 解耦的. 而从模块化的思路看, 组件状态应该在组件层面进行封装, 开发维护和使用过程当中都应该与其他的模块无关. 那么, 这个场景当中所谓"局部状态"就更应该指的是技巧上达成的局部, 而不是对简单地 MVC 进行取舍.
http://weibo.com/1651843872/E...
理解陌生技术的过程真可以用盲人摸象来形容, 大部分人了解到的永远只是局部, 而只能借助直觉, 从局部推演整体是什么样子的. 比如 Cursor 这东西, 在 js 里有 OOP 封装的写法, 在 cljs 里有 Atom 封装的写法, 在 Mobx 里思路类似但文档上介绍是 Observable. 我也郁闷了, 准确来说什么样?
试图解决的问题
之前 Respo 的方案当中, 组件状态是以 state tree 的形式储存的.
React 当中的组件状态是单独存储在每个组件当中, 在热替换不可靠,
于是我在 Respo 当中存储在了全局的一个 HashMap 当中,
同时, HashMap 的结构和 DOM tree 对应, 便于框架自动索引和绑定,
也就是说 state tree 对 DOM 结构存在依赖, 而且深度很大.
而且 state tree 是 Model, 这时 Model 对 View 存在依赖了, 不妥.
另一方面, 当 state tree 结构合理, 跨组件的状态操作是可能的,
比如组件 A 的状态在 state tree 某个位置, 通过 dispatch 进行操作,
这时 dispatch 只要知晓 A 状态的路径, 就能在另外的组件发起操作,
这种情况就需要人为对 state tree 进行设计, 以便结构能够被识别,
而且可识别的 state tree 可以被其他代码处理和分析, 也是好处.
当前依赖 View 也就是 DOM 结构的 state tree 无法达成这一点.
所以我的目标是提供手工设计 state tree 的方案, 以便项目自由控制,
并且最终结构清晰的 state tree 可以在另外的场景发挥作用.
当然, 同时也要满足一些约束, 首先 state tree 是全局的不用说了,
然后, 组件化开发中, 组件状态依然要做到解耦, 不能牺牲组件化,
总体概括, 就是简化 state tree 的实现, 暴露给项目代码.
给出的方案
实际得到的方案还有点麻烦, 可以说开发者的工作量变大了,
涉及到 Store 和 Component 部分的修改, 加上数据传递, 大致有:
组件可以获取到
states
变量, 是一个局部的组件树,
通过(:data states)
可以获取当前组件的数据,
同时 states
还包含内部的全部子节点的状态, 类似的格式存储.
组件可以获得一个
cursor
变量, 表示当前的组件状态在 state tree 的位置,
这个cursor
由 Respo 框架内部进行维护, 减少出错的麻烦,
实际上就是一些 keyword 组件的 vector, 可以用在 assoc-in
函数.
Store 当中约定
:states {}
字段专门用于存储 state tree,
这样的好处是达成了 single app state 这样的目标,
更新时, 拿到 cursor
和 next-state
在 state tree 上进行数据修改.
完整的 Demo 可以看我更新的例子, 作为演示而言还是太长了一些:
https://github.com/Respo/resp...
粗略可以看一点, 调用组件的地方麻烦了许多, 注意 with-cursor
,
主要专门声明创建一个 Sub Cursor, 这里用了 n
直接作为 key,
同时 states
也需要手根据 key 读取对应分支的数据:
(div {}
(->> (range 10)
(map-indexed (fn [index n]
[index (with-cursor n (comp-box (get states n) n))]))))
再看这个组件 states
需要手动传入和获取,
而 cursor
通过框架传入. 另外 state
也要手工计算得到:
(defn render [states n]
(fn [cursor]
(let [state (or (:data states) initial-state)]
(div {}
(comp-text (str n ". ") nil)
(comp-button "inc" (handle-click cursor (+ state n)))
(comp-text state nil)))))
而修改状态的步骤变麻烦了, 需要 dispatch 一个 :states
行为,
同时带上前面说的 cursor
和 next-state
两个数据:
(defn handle-click [cursor next-state]
(fn [e dispatch!]
(dispatch! :states [cursor next-state])))
最终由 updater
处理 state tree. 注意 mutate
只是语法糖:
(defn updater [store op op-data]
(case op
:states (update store :states (mutate op-data))
:inc (update store :pointer inc)
:dec (update store :pointer dec)
store))
按照这些代码, 基本上完成了 MVC 的一个循环, 而且代码量也大了很多.
了解 Cursor 的同学应该注意到这思路很像, 尽管写法差别很大,
如果是 Cursor, 都不需要 dispatch 这个过程, 只要调用接口操作即可,
我这里的原因是为了解耦, 也算是有自己的场景, 我需要 actions 独立出来.
具体到 Cursor 只能让参考已有的文档了, 我这里也描述不清楚:
https://github.com/omcljs/om/...
https://github.com/dustingetz...
依然存在不足
经过重构, 之前的目标大致算是达成了:
state tree 的结构可以通过人为控制, 更加清晰
跨组件的状态操作能够实现, 从 dispatch 里都可以做到
全局状态和组件化写法共存, 这是之前就达到的, 没有破坏
统一到了 single app state, 没有之前独立的 states 了
灵活性上其实还可以, 试着改了之前的代码, 基本确认可行.
不过麻烦的地方主要是在胶水代码的量太多了, 不好抽象掉,
这就意味着以后犯错的机会也会增多, 别人入门的困惑也会增多.
对新手而言, 他们巴不得是语法糖, 不要费力思考, 直接就能用,
而且所有人使用别人的代码时都希望依赖的信息越少越好,
从这个角度, Respo 目前状态管理的写法存在很多坑, 还得想办法填一下.