Respo 是我基于 ClojureScript 写的模仿 React 的一个类库.
之前有过文章介绍了, 起因是 React 社区对 cljs 的推崇,
cljs 是 Lisp 方言, 而且自带 Immutable data, 很实用写 virtual DOM.
我从三月初开始写, 中间断断续续做了优化和改进,
目前项目开始稳定, 并且积攒了一些经验可以整理出来分享.
目前 Respo 相关仓库我迁移到了独立的团队下进行维护:
http://github.com/respo-mvc
同时个人的前端页面 workflow 基于 Respo 创建,
我目前的 cljs 代码都和 Cirru Editor 绑定, 不熟悉 boot 的话会觉得费力:
https://github.com/mvc-works/...其他团队下用 Respo 实现的小页面也有, 目前活跃维护的有:
https://github.com/tiye/tiye.me
https://github.com/Cirru/resp...
https://github.com/Cirru/clou...
https://github.com/Memkits/wa...我自己已经能比较熟练地用 Respo 来处理大部分 React 的场景.
由于 Respo 对副作用限制比较多, 实际上关于动画和副作用不好做,
所以 Respo 其实只是适合单纯处理页面逻辑的应用.
最初为了服务端 diff 的噱头, Respo 是拆分了前后端的,
但这样就导致开发不便, 比如打断点之类的, 后端相当麻烦,
于是我做了一次重构, 所有代码合并到一个仓库,
服务端 diff 性能开销大, 而且估计也不会有人去用的...
合并之后引用模块稍微轻松了一点, 更新版本也是.
Respo 的 states 跟 React 不同, 是放在顶层次数的, 之前讲过,
之前说过顶层组件渲染是 f(store, states) = virtual-dom
一个纯函数,
内部组件同样是 f(args, states) = virtual-dom
的纯函数,
f(store, states, old-virtual-dom) = virtual-dom
意图就是用旧的 Virtual DOM 作为缓存, 避免掉一些冗余运算,
从思路上和 React 类似, 只是没有从局部触发, 而是每次从顶层组件开始处理.
最早 Respo 的 states 扁平的话, 但扁平的坏处是每次修改 states, 引用就变化,
引用改变结果我就无法通过快速的对比来优化渲染的性能了,
我后来改的是内部的结构, 最早是用组件节点作为 key, 一层的 HashMap,
修改之后, 用的是一棵树来存储, 这棵树对应整个 virtual DOM tree,
其实存储的体积增加了不少, 但同时也减少了引用的更改, 因而跳过了一些渲染.
总体说 Respo 性能本身是提高了, 然而距离 React 等框架还有一些距离,
我当时粗略测试了 2k 个节点的列表, 然后渲染, 有几百毫秒的开销,
考虑到 Respo 内部逻辑比 React 少, 这个结果显然是不理想的,
我注意到 cljs 代码本身实现某些 js 不存在的数据类型用到了函数调用,
比如我用 vector
替换掉 lazy-seq
后性能明显就提升了很多,
估计整体上还是有影响.. 语言层面就有性能差距, 而且我的代码还需要优化,
特别手机上节点增多之后计算的开销更明显, 以后还需要细看.
由于语言本身基于 Immutable data, 本来是非常轻松能完成热替换的,
然而增加了缓存以后, 经常出现一些缓存不能正常清楚的情况,
主要是热替换过程中 Virtual DOM 缓存仍然在, 而代码替换不完整,
比如说子模块 render
函数更新了, 父模块却不更新, 就不能判断缓存失效了,
我后来大致定位了是由于 boot-reload
对依赖更新处理不充分,
但上游认为不好重现不好解决, 我只能加入了 clear-cache!
允许手动清除,
也就是在热替换发生时强制清除缓存, 目前运行正常.
最早用的 DOM Diff 算法我换专门介绍了, 比较简单的算法,
基于 HashMap, 然后对 key 进行排序, 再一次循环搞定,
确定就是排序的过程在代码当中实现相当麻烦, 而且带来一些限制,
我后面想到了办法, 就直接去掉了, 业务中使用会简单一点,
由于新的算法会多次往后查找, 复杂度至少 O(n^2)
了.
当然之前也不快, sort 过程有开销加上 O(n)
的开销...
总体还是变慢了, 但对于开发来说还是合理的优化.
comp-text
文本在 Virtual DOM 当中以 <span>
的形式渲染, Respo 中更麻烦,
(span {:attrs {:inner-text "content"}})
用组件之后就是 (comp-text "content" nil)
, 稍微轻松了一些
comp-space
之前用 React 就在想怎么处理行内空白的问题, 一般都是用 margin
处理的,
然而 margin
在内容修改时很难维护, 所以我宁愿用额外的组件去做.
有性能开销, 但实践下来确实少了一些处理 CSS 的麻烦...
情况明确化其实还可以抽象一下, 减少维护上的成本.
comp-debug
其实只是一个 position: absolute
的 <div>
用来展示数据,
自从发现显示出来比打 log 更清晰, 我就一直在用这个组件调试,
只要 (com-debug data nil)
就能插入元素, 第二个参数还能控制样式.
未来还可以引入 respo-value
模块把数据的结构展开来显示.
和 React 当中使用 inline CSS 差不多, 只是自带 merge
稍微省事一些.
Respo 的 HashMap 和 if
非常适合对 style
进行处理.
另外还想到一个怪招来处理 :hover
状态的样式,
就是我直接把 <style>
标签 inline 到 DOM 树上了, 里边还是 CSS.
这个真的是夸张, 但是至少说明特殊场景并不是完全没办法.
还是回到上边插入空白的问题, 如果是列表, 怎样插入空白, 甚至是边框呢?
一般 CSS 的做法, 先对所有节点设置样式, 然后去掉头尾中一个的样式,
这个在 inline CSS 当中比较排斥, 而且语义上也挺奇怪的, 还要清楚样式.
我后来想到, 其实可以直接对 Virtual DOM 就行处理, 那就是个数据结构嘛,
可以直接访问 :children
在需要的位置加上 border 或者 space.
好处在于, Respo 基于 Immutable data, 不担心破坏 Diff. React 似乎不合适.
当然这个做法也会带来性能问题, 性能敏感的应用先不考虑.
这一点还可以再往远了想. React 开始玩 Higher Order Component 了.
其实 Component 像是函数, 但是做了缓存方面的优化,
但整体上说, 可以把 Component 和 Virtual DOM 想象成普通的函数和数据,
而函数和数据当中原来常常做的抽象, 自然在这里可以借鉴, 只要控制好副作用.
Respo 在这方面做得更好, 组件用函数语法调用, Virtual DOM 就是数据.
所以基于 Respo 还能做更复杂的一些抽象, 而不用有额外的担忧.
我关心的大致有几个方面, immutability, 灵活性, 性能, 错误提示,
前两者由于 cljs 本身设计优秀, 我认为已经达到目标了,
性能前面说了, 语言的开销加上代码实现的问题, 还要想想办法,
错误提示方面, cljs 本身是动态语言, 加上人工的检查少, 还是不足.
个把月当中应该很难有明显的突破, 而且代码已经稳定下来了...
暂时就这些吧, 希望后面能用 Respo 写出庞大的应用从而获得经验.