Respo-增加-Effects-功能支持

30次阅读

共计 2479 个字符,预计需要花费 7 分钟才能阅读完成。

补了一些关于 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] [x'y'] [action el]
  (println "effects"))

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

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

(defmacro defeffect [effect-name args old-args params & body]
  `(defn ~effect-name [~@args]
    (merge respo.schema/effect
     {:name ~(keyword effect-name)
      :args [~@args]
      :coord []
      :method (fn [[~@args] [~@old-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))

以往的纯组件

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,

clojure
{: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] 参数和 [a'b'] 参数, 可以用来判断具体参数是否发生改变,
比如 [title][title'] 如果数据一致, 可以自行判断跳过副作用.
然后是 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 被触发, 状态就未必一致了.

目前的实现当中, :update 是组件有重新调用就一定会触发的.
也就是是存在情况, effect 的参数没变时, 也会有调用,
这个行为可以再考虑一下, 后续大概可以关掉, 甚至整个对 effect 再做简化.
从 React Hooks 的使用看来, 旧的参数未必真的用到. 那么很可能是多余的.

而且如果出现需要监听的场景, 也要考虑一下组件当中的私有状态.
比如用 Atom 存储私有状态, 运行存放监听器, 从而再取消挂载时删除 …
没有明朗.

其他

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

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

正文完
 0