前言
这是一篇纯技巧向的文章,跟一年多之前的《揭秘 Vue-3.0 最具潜力的 API》一样_[0]_,更少的背景铺垫,更多的代码,更多的 demo,更快的节奏。
让我们直接进入主题。
背景
前一阵子,有开发者发推特表示用 Svelte 实现跟 React 一样的功能_[1]_,代码简洁很多。
他给出的 React 代码如下:
而 Svelte 代码如下:
用了复杂的 react 处理,佐证 svelte 的优越,这不是一个很公平的对比。
然后,Vue 也加入了试炼 [2],尤小右给出了他的代码:
跟上面 Svelte 版本一样简洁。
而后,小右又提供了 ref-sugar 版本,更为精炼。
据说,在推特上:
React 实现一堆人捣鼓了半天也没弄出个能在 el ref 变更时候重新 observe 的版本...
一、渲染友好的 React-Hooks 公式
每当 React hooks 被拉出来溜的时候,一般都是负面的。确实很容易写出性能有问题的代码。
关键是,大家太轻易使用 useState 了。
在 vue-composition-api 中,reactivity 数据都有 wrapper,比如是 reactive 和 ref。
useHeight 等 custom-vca 里不管产生多少个 reactivity 对象,不会直接产生 re-render。
只有那些被 return 到最外部,跟 template 绑定的部分,会触发视图渲染。
而 react 的 reactivity 就是通过 re-render 实现的,useState 没有 wrapper,一用就得到一个触发渲染的函数。
在这种 reactivity 机制下,需要特殊的心智模型去编写代码—— State/Effect 分层。
useHeight 如果是下面这样:
let [ref, height] = useHeight()
高度变化时,被动 re-render,难以过滤,难以转换,难以合并。
大部分情况下,不是提供 state,而是提供 effect,可能更好。像下面这样:
let [height, setHeight] = useState(0)
let ref = useHeight((height) => {
// do something with height
setHeight(height)
})
由使用者去外部声明 state ,然后在 useHeight 的 effect callback 里按需 setHeight。
更新权反转。
使用者可能结合其它状态,做 dispatch 到 reducer 里的整体一次性更新,而不是被动 re-render。
可惜目前 React 社区还没建立清晰的 State/Effect 区分。
我们可以根据 State/Effect 分层理念,尝试给出 React hooks 渲染友好的公式:
let handler = useProducer(consumer, options)
producer 生产者接收 consumer callback 消费者回调作为参数,返回 handler 控制权函数,用以绑定到事件或者其它 consumer 位置。
以 useHeight 作为案例,验证上述公式的效用。
二、React 实现
我们要实现的功能如下:
给定一个 textarea,它是 resizable 的;我们追踪它的尺寸变化,并展示到文本里,同时我们增加一个 checkbox 控制,可以切换监听 / 不监听。同时,我们只监听一定范围内的尺寸变化,而忽略超出范围的。
即更精细的控制:
1)控制监听 / 非监听;
2)控制监听的范围,只响应符合要求的渲染需求。
功能如下:
我的代码实现,按照从 low-level 到 high-level 层次迭代上来。
首先实现 useResizeObserver,一个对 dom api 的 low-level 适配。
返回 3 个控制函数:
1)trigger 决定监听哪个元素;
2)enable 启动监听;
3)disable 禁用监听;
useResizeObserver 的实现思路,跟大家之前惯常做法不同,它不返回 state 出去,不会引起 re-render,而是调用 callback(target),将 resize effect 暴露出来。
然后,我们基于 useResizeObserver 实现 useHeight,将 observe 的关注点缩小到 offsetHeight 里,并提供 getCurrentHeight 的新方法。
同样的思路,我们还可以实现 useWidth 等其它监听。
使用时,在 useHeight(onHeightResizeEffect) 里,按需将 currentHeight 同步到 setHeight 里。
通过 observer.trigger 决定监听哪个元素,并提供 enable/disable/getCurrentHeight 等控制函数,通过 useEffect 监听,在合适的实际调度不同的控制。
以上,渲染友好、控制精细的 React-Hooks 版本完成。
react-resize-observer 的 demo 地址
代码或许比 Svelte 和 Vue 的版本长一点,但 State/Effect 的这种实现思路是通用的,在 Vue 里也适用。
下面演示,基于相同理念,分别用 Rxjs,Vue 和原生 JS 实现相同功能。
三、Rxjs 实现
实现 rxjs 版本的 use-height。也要支持:1)切换 trigger;2)切换 enable/disable;3)在 enable 状态下,filter 部分 height 不做响应。
功能如下:
先实现 resizable,适配 low-level 的 dom api 为 rxjs 的 observable 版本,对应之前 react-hooks 适配的 useResizeObserver。
然后实现 onHeightChange,通过 subject 反转控制,switchMap 实现切换,返回 state$, trigger, enable, disable 四个字段。
这里返回 state 不是 state,它的 subscribe(consumer) 里就带一个 consumer。性质上是一样的。
通过 subscribe(state$) 去消费数据,通过 rxjs operators 过滤 / 转换 / 转接数据流。在其它 observable 数据流里,调用 handler 里的控制方法,disable/enable/trigger 即可。
rxjs-resize-observer 的 demo 地址
接下来,实现 vue/reactivity 版本的 use-height。思路跟 react-hooks 版本和 rxjs 版本也是一样。
四、Vue 实现
先实现一个将 resize-observer 这个 low-level api 适配为 vue reactive state 的函数。
定义一个 shallowReactive,监听 state.trigger 的变化,实现切换 resize-observer 的功能,并将 target 发送给 onResize。
再实现一个 onHeightChange 函数,拓展 ResizableState 为 HeightChangeState,增加 status 字段提供 enable/disable 切换,增加 height 字段。
上面做了两个 effect 的组合:
1)将 onResize effect 根据 option.fiter 映射到 state.height;
2)将 state.trigger 同步给 resizeState.trigger
onHeightChange 使用方式如下:
onHeightChange 定义了一个 state-effect bindings,但没有指定监听哪个元素,也没有指定如何消费数据。
通过 state.trigger 赋值决定监听谁。
通过 effect 决定如何消费。
功能如下图:
细心的同学可能已经发现了,上述 Vue 代码,只用到了 @vue/reactivity 包,而没有用 vue 视图相关的代码。
没错。
vue/reactivity 自身已经足够强大,可以独立实现很多功能。在《揭秘 Vue-3.0 最具潜力的 API》有更多解读。
这里提供一个新的应用潜能方向。
五、Vue 的 State-Effect-Bindings 模式
vue/reactivity 可以实现一种 reactive effect bindings 模式,可以概括为 reactive({args, returned}) & effect。
所有 function,都有 args 参数和 return value;将它们映射到一个 reactive state 中去,可以做到,state.args= 设置时,重新执行 effect,将结果映射回 state.returned。
这个模式对前端开发者非常友好,因为 dom api 大部分就是这样的。
elem.style.color = 'red',触发渲染 effect,将结果同步给 computedStyle(elem).style。
回到 vue-resize-observer,上述模式对应的是:
resizable 的 state.trigger 其实是 ResizeObserver.observe 的参数,它的返回值被定义为 elem 本身,不符合 vue watch 的条件,因此,从 state.target 字段,变成 onresize effect callback。
当 effect 可以同步到 state 时,我们采用 state;当 effect 难以同步到 state 时,我们采用 effect callback。
onHeightChange 的 on height change effect 可以同步到 state.height,因此我们取消了 effect callback。
可以拓展 filter 等增强操作,去增强 effect -> state 同步的细节控制,拓展 state.status 等附加信息等。
当整个 vue app 由 state-effects-bindings 模式来构建,理想情况下,最后将自动形成一个大的 app state,整个应用状态自动可配置化。
appState.menu.status = 'on' 就打开了菜单。
appState.loading.text = 'xxx' 就展示了 loading。
effect(() => appState.menu.status) 就监听了菜单变化。
并且这个构建过程,不是自上而下的,不是先定义一个 global app state 然后在这个约束下去分配 state 给 sub-component/model。
可以是自下而上的,每一部分只处理自己关心的部分,并将 state/effect 的能力暴露出去,由更大的部分去将 state/effect 通过 bindings 映射到更大的 state/effects,如此不断整合,最后形成了一个 app state
如 resizable 只提供了 state.target,而 onHeightChange 在此基础上拓展了 {height, status} 等状态。
六、原生 JS 实现:手中无框架,心中有框架
当我们认识到,问题无非是 state-effect-bindings 的不断冒泡时,有没有 react-hooks,有没有 rxjs,有没有 vue-reactivity,不是必要的。
先实现 Resizable,接受一个对象参数,返回一个对象。对象参数是 visitor/consumer,返回对象是 handler。
基于 Resizable 实现 HeightChange,接受更多字段的对象参数,返回更多方法的对象。
然后在 consumer 参数中,指定如何消费数据,如何转换数据,如何过滤数据。
在 event listener 等 bindings 中,指定如何调度 handler 方法。
vanilla-resize-observer 的 demo 地址
功能如下图:
尽管用原生 JS 也行,但有 react-hooks, rxjs, vue-reactivity 也是有帮助的,此处主要展示 State/Effect 分层理念在 UI 开发里的通用性。
总结
如上,我们演示了 react, rxjs, vue, vanilla-js 在 State/Effect 分层理念下,实现相同功能的代码实现。
我们可以在 State Management 概念的基础上,再增加一个 Effect Management 的概念。
正如我之前在某个知乎问答里所说的:
1)当你的项目数据复杂度很低,用 react 自带的 component-state 就可以
2)当你的项目数据复杂度一般,lift state 到 root component,然后通过 props 传递来管理
3)当你的项目数据复杂度较高,mobx + react 是好的选择
4)当你的项目数据复杂度很高,redux + react 可以帮助你维持可预测性和可维护性的下降曲线不那么陡。所有 state 变化都由 action 规范化。
5)当你的项目数据复杂度很高且数据来源很杂,rxjs 可以帮助你把所有 input 规范化为 observable/stream,可以用统一的方式处理。
思路其实很简单:
1)当 UI 变化很复杂时,用 component 归一化处理,即 View-Management
2)当 state 变化很复杂时,用 action/state 归一化处理, 即 State-Management
3)当 data-input 很复杂时,用 rxjs/observable 等概念归一化处理,即 Effect-Management。
任意问题,只要足够普遍和复杂,就值得抽象出专门化的机制。
Effect Management 在前端开源生态里,还不是很繁荣,是一个大家可以努力的方向。不仅 rxjs 可以做 effect 管理,vue/reactivity 和 react-hooks 乃至原生 JS 裸写都能做,有待大家的发掘。
更具体地说,前端社区可以往下面几个方向努力:
1)基于 react-hooks 和 State/Effect 分层理念重新梳理哪些应该暴露为 effect,哪些应该暴露为 state,提供一个性能更加良好的版本(只能用在 react 体系)。
2)基于 vue/reactivity 和 State-Effect-Bindings 理念,实现的 state/effect 管理框架(脱离 vue 视图框架也能使用)。
3)基于 rxjs 和 State-Effect-Observeable 模式,实现 state/effect 管理框架(可以配合任意视图框架使用)。
期待有同学能在上述方向上产生成功案例~
引用来源:
[0] 《揭秘 Vue-3.0 最具潜力的 API》
[1] react 和 svelte 实现 use-height 的对比
[2] vue 作者的 use-height 实现