Respo 近期归纳

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 算法

最早用的 DOM Diff 算法我换专门介绍了, 比较简单的算法,
基于 HashMap, 然后对 key 进行排序, 再一次循环搞定,
确定就是排序的过程在代码当中实现相当麻烦, 而且带来一些限制,
我后面想到了办法, 就直接去掉了, 业务中使用会简单一点,
由于新的算法会多次往后查找, 复杂度至少 O(n^2) 了.
当然之前也不快, sort 过程有开销加上 O(n) 的开销...
总体还是变慢了, 但对于开发来说还是合理的优化.

常用组件

  • comp-text

文本在 Virtual DOM 当中以 的形式渲染, Respo 中更麻烦,
写法长达: (span {:attrs {:inner-text "content"}})
用组件之后就是 (comp-text "content" nil), 稍微轻松了一些

  • comp-space

之前用 React 就在想怎么处理行内空白的问题, 一般都是用 margin 处理的,
然而 margin 在内容修改时很难维护, 所以我宁愿用额外的组件去做.
有性能开销, 但实践下来确实少了一些处理 CSS 的麻烦...
情况明确化其实还可以抽象一下, 减少维护上的成本.

  • comp-debug

其实只是一个 position: absolute

用来展示数据,
自从发现显示出来比打 log 更清晰, 我就一直在用这个组件调试,
只要 (com-debug data nil) 就能插入元素, 第二个参数还能控制样式.
未来还可以引入 respo-value 模块把数据的结构展开来显示.

inline CSS

和 React 当中使用 inline CSS 差不多, 只是自带 merge 稍微省事一些.
Respo 的 HashMap 和 if 非常适合对 style 进行处理.
另外还想到一个怪招来处理 :hover 状态的样式,
就是我直接把