Respo 增加 Effects 功能支持

补了一些关于 Respo Effects 的文档, 英文细节有点吃力,
https://github.com/Respo/respo/wiki/defeffect
关于 Respo 的设计思路和功能取舍, 这边可以再描述详细一些.

新增的写法

比方说有个组件要增加副作用,

(defcomp comp-a [x y z]
  (div {}))

这次更新以后, respo.core 当中新增了一个 defeffect 的宏用来定义副作用,
defeffect 需要的不单单是多个参数, 而且是很多组参数.

(defeffect effect-a [x y] [action el *local]
  (println "effects"))

[x y] 当然就是参数了. 框架渲染过程当中会自动插入参数的值,
另外框架会插入 action 表示 :mount :update :unmount,
以及 el 是组件根节点, 也是由框架获取.

这个宏的实现, 就是把代码转换成一个函数, 函数返回的是个 HashMap,

(defmacro defeffect [effect-name args params & body]
  `(defn ~effect-name [~@args]
    (merge respo.schema/effect
     {:name ~(keyword effect-name)
      :args [~@args]
      :coord []
      :method (fn [[~@args] [~@params]]
                ~@body)})))

上面定义得到的 effect-a 就是一个函数, 可以通过 (effect-a x y) 调用,
在组件当中使用的时候, 就是把返回值变成数组, 在数组当中加上副作用

(defcomp comp-a [x y z]
  [
   (effect-a x y)
   (div {})
  ])

后面就依靠 Respo 的渲染代码, 内部进行判断, 对 effect 进行处理.

由于 effect 没有直接区分开不同的生命周期, action 使用时需要自行判断,

(case action
  :mount (do)
  :update (do)
  :unmount (do)
  (do))

*local 的存在, 是为了应付可能存在的存储局部状态的需求.
比如在 mount 的时候创建的数据, 如果在 update 和 unmount 需要用到,
目前的设计当中, 就需要组件提供私有的状态用于传递.
需要注意, 这个 *local 实际上对应的 React 当中的 ref,
也就是说, 在 *local 上修改数据, 不会出发 rendering 的行为.

以往的纯组件

React 当中组件定义的方式比较简单,

(defcomp comp-a [x y]
  (div {}
    (div {} (<> "DEMO"))))

然后会经过一次宏展开, 宏的实现是

(defmacro defcomp [comp-name params & body]
  `(defn ~comp-name [~@params]
    (merge respo.schema/component
      {:args (list ~@params) ,
       :name ~(keyword comp-name),
       :render (fn [~@params]
                 (defn ~(symbol (str "call-" comp-name)) [~'%cursor] ~@body))})))

上面的组件经过 (comp-a x y) 这样的调用之后, 会得到一个 HashMap,

{:name :comp-a
 :args '(x y)
 :render (fn [x y]
           (defn call-comp-a [%cursor]
             (div {}
               (div {} (<> "DEMO")))))}

可以看到其中没有实现生命周期的信息.
这个高阶函数在运行时会继续被处理, 添加所需的参数, 再被计算.

这个结构当中并没有预留跟 React 相似的组件生命周期,
而且也不适合用方法进行扩展, 所以比较难直接有 React class 组件那种写法.

想法和尝试

如果需要在组件当中支持副作用的, 至少要在组件的表上加上 effects 的位置,

{:name :comp-a
 :args '()
 :render (fn [])
 ; add
 :effects [(fn [])]}

原先的 defeffect 的 API 写出来, 我大致确定了需要哪些参数,
比如 [a b] 参数, 界面渲染和更新当中使用,
然后是 action 用来判断生命周期, el 对应 React 当中的 DOM Ref 用.

有了 defeffect 之后我在考虑, 都是把 effect 插入在 DOM 树当中,
类似 (div {} (effect-a x y) (div {})) 这样,
但具体看了实现, 涉及到 DOM Diff 的实现有很多坑, 也就作罢了.
于是想怎样才能以兼容已有的写法的方式吧副作用插入进去.. 最简单就是数组了.
用数组的话, 可以插入多个 effect, 并且后续也有些许继续扩展的能力.

这套写法跟 React Hooks 比起来, 有不少的功能缺失.
特别是 Respo 当中, 基本没有运行渲染过程再 dispatch actions 的可能.
React 当中频繁有 componentDidMount 或者 useEffect, 在任何时候修改组件状态,
而且也没有限制在这种生命周期时 dispatch actions.
Respo 里不认可这样的做法, 这样会持续衍生出 actions 来.
特别是在 Time Traveling 的场景当中, 这种 actions 就是破坏性的,
一旦切换到旧的某个 action 导致新的 actions 被触发, 状态就未必一致了.

整体考虑为了热替换方便, 组件局部状态的变化, 是不鼓励的.
目前 Respo Effects 算是出现在早期状态, 后面也可能再调整.

其他

不管怎样, 此前 Respo 为了实现纯的渲染, 没有做 effects,
导致跟 JavaScript 生态已有的一些用法不能轻松衔接.
现在加上了 Effects, 那些东西终于可以进行尝试了.

Respo 最初版本是 2016 年初开始的, 年中基本完成,
这么多年了, 用的人少, 这方面的需求也没有太大的问题, 因为场景也有限.
我个人觉得 Effects 不会有太多的需要. 还是以小范围扩展功能位置.

阅读 397

推荐阅读
题叶
用户专栏

ClojureScript 爱好者.

500 人关注
251 篇文章
专栏主页