纯技巧向:React, Vue, Rxjs 和原生 JS 代码大乱斗

15,109 阅读9分钟

前言

这是一篇纯技巧向的文章,跟一年多之前的《揭秘 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而不像reacthooks版本接受consumer是因为,state 而不像 react-hooks 版本接受 consumer 是因为,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-resize-observer 的 demo 地址

功能如下图:

细心的同学可能已经发现了,上述 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》

mp.weixin.qq.com/s?__biz=MzA…

[1] react 和 svelte 实现 use-height 的对比

twitter.com/AdamRackis/…

[2] vue 作者的 use-height 实现

twitter.com/youyuxi/sta…