本文基于 https://reactjs.org/docs/hook... 功能展开
算算时间都要一年半了, React 在 2018 年推出 Hooks, 引发了热议.
印象里就是在群里面, 我就很纠结里边的黑魔法太奇怪了.. 看得小心翼翼的.
然后看着别人研究代码, 提出类似的实现之类的, 或者各种解释.
慢慢地很多不同的声音也发出来, 特别是迷之闭包, 很多人都中招了出来吐槽.
后来呢, React Hooks 蔓延开来, 连 Vue 社区也开始模仿.. 看来是真重要了.
由于我没有动力去深入 React 完整的实现, 所以对细节也只是处在一个大致的了解的状态.
对于 Hooks 想要解决的问题, 我大致是认同的, React 的扩展功能太僵化了.
高阶组件, 虽然在 FP 里面牛逼哄哄的名字, 实际设计起来 decoration 满天飞,
..太奇怪了, 一个 FP 里引入的概念, 用大量的 OOP 和 mutable 方法来实现.
而且去掉了 mixin 机制之后, React 复用逻辑的问题感觉就是个坑, 重复代码挺多的.
虽然我是不信任 React 搞这种黑魔法推翻以前的一些宣传口号的, 但是...
Hooks 确认可以帮我解决 class based 组件难以做好的逻辑复用的问题.
我在公司里推 Hooks 的时间比较晚, 已经是在活动听到别人折腾 Hooks 之后了.
最开始是 Ajax 代码复用, 网上当初那个例子很明显, 现在我们也抄了很多这种.
但是后来, 让我感受到最大变化的, 还是发现 Hooks 对我们的 Form 组件的改善,
可以先看例子 http://fe.jimu.io/meson-form/...
大致上说, 就是用 Hooks 抽离状态的话, 复用场景更加灵活, 超出组件层面...
这篇文章是我几个跟 Hooks 相关的想法梳理, 线索有点乱, 看章节吧.
我们知道 Hooks 最开始就明确说了, useState
等 API 调用, 跟依赖相关, 且必须是静态的.
原因也不难明确, React 组件运行过程中要逐个追踪, 动态性会破坏这个逻辑.
我用 ClojureScript 的时间挺长了的, FP 当中一般的玩法我是知道的,
一般来说, 为了代码的"引用透明", FP 当中会避免四处存在内部状态.
一个用 Hooks API 维护状态的函数组件, 本身居然有这样的状态, 很反 FP 常规的玩法.
而且这个, 虽然现在是习惯了, 但是总归是在有一些限制的, 令人忌惮.
换个角度的话, 也不能说 FP 里面就没有这种状态的性的东西.
而是说, FP 当中状态是习惯于显式跟普通的计算区分开的,
你要用状态, 就明确声明这边有个状态, 大家调用都注意. 不然, 那就是纯的计算.
或者, 再换一个层次说, 你这不是代码, 就是 DSL, 这个 DSL 提供新的一层抽象.
DSL 相当于构建一套原先的代码之上的一层新的语义, 那无所谓 FP 不 FP 了.
可以把深层的逻辑通过复杂的手段约束在 DSL 语义内部, 上层就是调用 DSL.
应该说我理解 React Hooks 就是这样的一个状态, 语言之上的 DSL.
作为 DSL 说的话, 我觉得 Svelte Vue 那样倒是更自然了.
倒不是说它们提供的方案一定比 React 好, 但是有一个编译阶段, DSL 就更完善,
首先 DSL 某个语义未必就是一个函数就能实现的, 可能需要些比较绕的代码,
其次, 提供语义也就意味着对用户的术语有约束, 就需要编译之前做验证,
但 React Hooks 这边, 这就是 runtime 插入功能然后让人们自己去做限制了.
当然, 我持有的观点, 有一点是 JavaScript 没有 Macros, 限制了 Hooks 的设计.
我用 ClojureScript 作为 Lisp, 比较容易修改语法树, 展开简单的代码,
...应该说 Lisp 这种, 能力也远不如 Babel 甚至 Svelte 那么强大, 只能说廉价.
Hooks 的设计, 在增强功能的同时, 很大程度是想着保留 API 的简洁.
如果有 Macros 可以用的话, Hooks 可以考虑的写法还有很多,
比如说定义组件的时候写成,
(defcomponent c-demo [x y] (div {} (str x y)))
然后由 Macros 系统展开成函数, 并且由执行环境诸如几个变量(影响到 ts 什么我就不管了),
(defn c-demo [x y] (fn [use-state use-effect internals] (div {} (str x y))))
我们目前需要全局引用 useState
useEffect
然后心里去想着那是运行时的东西如何如何,
但是从上面的展开例子, 从 Macros 的角度理解, 很容易知道这些是运行环境控制的操作,
这样就没有原先 React Hooks 那种让人产生些许错觉的写法了.
(有可能语法展开跟函数执行的区别对非 Lisp 程序员比较难区分... 大致按 Babel 去想吧.
另一个是状态追踪的问题 Hooks API 依赖的是 useState
的顺序.
我个人比较倾向于用名称去控制这多个状态, 在用户端有更多控制能力.
而这部分在跟 Vue 和 Svelte 对比会发现 React 这边能设计出更多的花样.(没去了解具体实现)
而 React 使用函数作为唯一的手段, 就显得非常, 或者还是逃不脱被 DSL 要求存在的限制.
当然话说回来, React Hooks 单调的方式能被设计出来满足这么多功能, 也是超乎我想象了的.
同时也由于抽象手段单一, 就非常依赖 runtime 内部实现奇奇怪怪的手法去支持功能.
Respo 是我个人项目使用的 cljs 之上的 Virtual DOM 方案. 并不基于 React.
这样我就有机会试验我自己思考的一些组件和状态的抽象方案.
Respo 里边, 为了保存热替换过程中组件的状态, 设计了用户态的状态树的概念,
用户在定义组件的时候, 大致上对一个 Todolist 会在全局构建和存储这样的树形结构:
{ :data {} :todolist { :data {:input "xyz..."} "task-1-id" {:data {:draft "xxx..."}}} "task-2-id" {:data {:draft "yyy..."}}} "task-2-id" {:data {:draft "zzz.."}}} }
组件代码当中需要比较啰嗦的声明,
; for app (comp-todolist (>> states :todolist)) ; for single task (comp-task (>> states (:id task)) task)
另外具体使用还要比较啰嗦的声明,
(let [state (or (:data states) {:input ""}]) "TODO whole list") (let [state (or (:data states) {:draft ""}]) "TODO Task")
以及在组件层级之间传递 cursor
的路径位置, 以便发起更新...
相比 React 任何一种多了很多的代码, 只能说为了热替换稳定做了非常拙劣的模仿,
然后, 再次之后, 我也能在这个方案上, 也提供类似 Hooks API 的状态抽象,
(defn use-menu-feature [states] ; TODO {:ui "TODO", effect-fn "TODO", :edit-fn "TODO"}) (menu-A (use-menu-feature (>> states :menu-a) {})) (menu-B (use-menu-feature (>> states :menu-b) {}))
这是一个脱离了 Macros 和编译方案, 也脱离了 React 内部状态黑魔法的方案,
而这样简单啰嗦的方案, 切切实实也能模仿出 Hooks 核心的一些功能来.
(当然在整体功能上, 跟 React 不能比, 而且要实现的话也依赖 runtime 做.)
这个例子对于 React Hooks 本身没有什么帮助或者阐释,
主要是从另一个角度去看, 作为一个前端 MV* 方案, 怎么看待其中的核心需求和实现.
各种方案在各种需求点的路径探索以及取舍, 脱离一下限定的视角, 会有其他主意.
回到 Hooks 本身, 我目前在的业务当中探索使用的方案, 大致可以参考 Form 这个例子,
let formItems: IMesonFieldItem[] = [ { type: 'input', name: "name", label: "名字", required: true }, { type: 'select', name: "city", options: selectItems, label: "城市" }, ]; let [formElements, onCheckSubmit, formInternals] = useMesonItems({ initialValue: {}, items: formItems, onSubmit: (form) => { console.log('After validation:', form); }, });
或者也参考弹出提示的这个例子,
let [ui, waitConfirmation] = useConfirmModal(); let onClick = async () => { let result = await waitConfirmation({ text: "节点可能包含子节点, 包含子元素, 删除节点会一并删除所有内容.", }); console.log("result", result); } return <div> <button onClick={onClick}>Confirm</button> {ui} </div>
相较于 React 组件以往的抽象方案来说, Hooks API 暴露出来了一个从前我们熟悉的功能.
就是: 可以修改抽象模块的内部状态了.
以往 React 宣传的组件, 作为一个抽象, 内部的状态是封闭的, 外部不应该去操作.
比如就一个弹窗的 Modal
, 你就需要外边有个 visible
的状态去控制, 传进 props, 显示, 还是不显示,
但是有了 Hooks, 这时候你可以把状态封进 Hooks API 内部, 然后暴露回调函数去操作.
可是你又明显能知道, 这边封装了一个状态.
至少从前 React 并不鼓励这种状态, 或者应该说, 此前并不存在简单的这样的功能.
反观我们实际业务当中, 局部的状态却是很常见的东西,
比如 visible
, 以往我们通过插件去实现的时候, 不就是把状态藏在插件内部的吗,
然后得到一个 .toggle()
的方法, 然后可能还会多个地方去 if (modal.visible) {}
一次次匹配.
当 React 提出需要一个父组件存一个 visible
还有加一个 onChange
回调的时候, 让人觉得很怪异,
最终我们希望暴露给业务当中使用的时候, 明显 .toggle()
才是极为简短清晰的方案.
顺着这个思路, 诡异的事情来了, jQuery 时代, 我们欢快地 .toggle()
,
React 来说, 说 rethink, 然后我们加了 visible
和 onChange
, 然后觉得 React 说得对,
现在基于 Hooks 方案, 封装局部状态的方案又开启了... 历史又陷入螺旋了.
...今天来不及写了, 我的感觉就是 React 选择 FP 这条路的时候, 其实没那么清晰,
探索了那么多方案, 强行把函数式的一些说法套用到前端这边来, 可能很多人都不能理解准确吧,
你说是 stateless 还是 state isolation, 细小的差异, 没有梳理清楚.
真的函数式语言, PureScript Elm 怎么写的前端, 真的是 React 这样子的吗?
在状态这个事情上, React 最终还是向新的方案妥协, 而且未来谁说的得准要不要再妥协一次呢.
至于 Virtual DOM 带来的 Model->View 那种 date-driven 的模式,
或者换个装逼的说法 "DOM 更新方案的自动化进程", 从 Angular/React 实打实普及到了整个前端领域.
这一点上没有多少分歧吧... 睡了睡了.