useReducer —— 是的,数据驱动乃控熵大法

(但是,只在封闭系统生效!)

早些时候,我对 hooks 下使用 useReducer 或者一众状态管理库是非常方案的,带上在结合项目仔细思考之下,发现了这一方案的优点,或许结合使用才是最优解

对 useReducer 的产生误解的原因也很简单:

  1. 类型支持非常差(js/ts 没有很好的逆变类型支持,没有模式识别,对比 reasonml 下的 useReducer 可知)
  2. 无模式识别(同上)
  3. 无法在 useReducer 内部,利用第三方 hooks 生态(即封装状态逻辑视图的全功能第三方库)

很简单一个例子,const {data,error} = useSwr(key,fetcher),这段代码将请求过程,全部转化为数据驱动的 data,error 状态,同时响应式支持了请求,延迟,屏幕聚焦,轮训,防抖等等功能

开发过程中使用这样的 第三方 api 能够得到事半功倍的效果

但是,如果你在这样的逻辑上,使用了 useReducer ,那么,这个叫做 useSwr 的 api,你无法再使用了,因为 reducer 内部代码(脱离了 react 调度

当然,这里说的是 useReducer 的劣势,redux,mobx 等工具不在讨论范围内,,因为缺乏 module 功能,没有初始化的控制能力(即状态管理功能无法随着组件的初始化而初始化,迭代,异步渲染中失去了可编程性),导致大部分场景下已经缺乏使用价值(除了跨平台,又想要省略 dsl 编译过程,不过这种需求非常罕见,不是正道,比如 rn 和 flutter 方案,flutter 原理上更胜一筹)

不过,如果缺乏专业产品经理或者业务专家,没有设计,叠加新版本相关生态不够完善的情况下(比如 antd 的 useForm 不是无视图依赖的逻辑api,导致 @testing-library/react-hooks 无法使用),redux 等工具的调试方式可能更适合不稳定项目(生态完善的情况下,依然优先 TDD,即便缺乏顶层设计,因为逻辑本身是抽象的,靠打印进行的调试隔着一层 dom,一层 vm,绝对不是好的选择,直接调试逻辑才是标准流程)

这么说来,useReducer 在 hooks 环境下,就毫无作用可言了么?

前文有提过,至少 react 所举的例子,并不能说明 useReducer 的意义:

截屏2021-05-29 下午9.16.36.png

仅仅是因为 useCallback 有变化?就需要用 useReducer dispatch?

const [todos,dispatch] = useReducer(todosReducer)
return <TodosState value={todos}>
  <DeepTreeWithState/>
  <TodosDispatch.Provider value={dispatch}> 
    <DeepTreeWithDispatch />
  </TodosDispatch.Provider>
</TodosState>
复制代码

甚至标准做法还应该做的这么丑陋?

拜托,这个实在误人子弟了,控制 view 永远靠 useMemo,调度逻辑也是业务逻辑的一部分,哪能就这么回避掉的?

直接返回未拆分的 jsx 本身的含义就是 —— 视图跟随所有 state,props,context 变化,逻辑即如此,反而 dispatch 虽不说不伦不类,也有点反直觉

useReducer 该用么?现在告诉大家,某些情况下的确该用,但是不是 react 文档提到的这种情况(至少绝不会像他说的那么简单)

什么情况下需要用到 useReducer ?

封闭系统下的 控熵

熵:泛指某些物质系统状态的一种量度,某些物质系统状态可能出现的程度

可以近似理解为,同一时刻,物体可能状态的数量,其中既包含状态的 数量,也包含状态的 可能性

还记得前文总结的么?React 虽然在 hooks 这一次更新,做了很大的函数式改进,但是数据驱动原则始终未变

毕竟直接由具象转化为抽象编程,跨度太大,前端又多是处在视图开发层次,状态或许比事件更加重要?

然而函数式是不应该有太多状态,无共享状态甚至是无状态的,因为函数本身是对变化的抽象,用状态模拟概念编程,补足函数式顶层设计短板的做法,其实是有很大牺牲的(比如缺乏概念编程的自解释性,类型无协变,即类型无法自顶向下等)

但是,现实是,前端必须两者兼得,必须尊重状态,因为前端开发本身就是中介,本身就是视图和一致性数据的中间件,状态绕不过去

同样,即便是事件驱动(zone)著称的 angular,也没有像 cyclejs 那样无状态流一撸到底,cyclejs 的不温不火也能反过来证明这一点

那么, 状态,以及背后代表的意义,就耐人寻味了

因为

状态就是熵

下面这段表述我相信对 react 程序员来说,都是入门经典:

截屏2021-05-29 下午9.36.59.png

没错!曼妥思糖和可乐!

曼妥思碰到了可乐,双方产生剧烈反应,一方的不确定性和状态数量会直接灌入另一方中,这就是个剧烈的熵增反应

image.png

左侧是你的应用中的状态,右侧是你收到的数据请求的状态,一旦两者想触碰,会发生什么?

注意,将数据和数据变化的可能性,都要作为状态统一看待,熵本身就是状态可能性集合,因此,除了请求返回数据,还有请求返回时间,错误,多个请求的关系,race 还是更加复杂的调度?

一边,是确定调度,确定结构的 react 应用,另一边,是不确定数据,不确定调度的异步

将两者看做一个系统,怎么解决这个问题?

没错!麦克斯韦妖!

image.png

假设出现一个有选择能力的主体 —— 麦克斯韦妖,它选择性地将结果放入另一个结构(react 应用/store)中,就可以解决这个问题

一边是确定性(低熵)的 state,通过这个 麦克斯韦妖(dispatch)选择 (reducer),很好地保证了 store
的低熵环境

const [state,dispatch] = useReducer((state,action)=>{
  if(action.name === 'xxx'){
    // 麦克斯韦妖/reducer 正在进行选择
  }
  
  // 麦克斯韦妖/reducer 正在放入另一侧的 state 
  return {...state}
},{})
复制代码

因此,前文提到的某个情景 —— 即应对非合成事件 的时候,callback 调用烧脑的问题(区分内外,区分调度源),useReducer 是个非常好的解决方案

这个时候,dispatch 的不变性,才有了很好的使用场景:

const ws = useRef()
const [state,dispatch] = useReducer((state,action)=>{
  if(action.payload === 'init'){
    ws.current = new WebSocker('...')
  }
  if(action.payload === 'read'){
    ws.current.send('read')
  }
  // ...
},{data:'',error:null})

// 这样,应用本身的结构,可以很容易(effect,合成事件)与另一部分(非合成事件)进行交流

useEffect(()=>{
  if(ready){
    dispatch('init')
  }
},[ready])


return <button onClick={()=>dispatch('init')}>init</button>
复制代码

这样的话,不确定性会比纯粹使用 useCallback 低得多(因为依赖很难得到处理,至少很烧脑)

整个应用的熵得到了非常好的控制

useReducer,系统对接的神器

是的,当我们想要将一个封闭系统,与另一个系统对接,形成更大的封闭系统时,useReducer 就非常有用了,因为它控制住了不确定性

将 dispatch 想象成麦克斯韦妖,你就能很轻松实现多个系统之间的隔离,并且有可预知的结果

那么,除了异步以外,还有哪些地方可以用到呢?

跨模块通讯!

这部分以后可以继续分享,因为涉及一个笔者仍没有彻底弄懂的架构技术

那全用 useReducer 可以么?

本来是可以的,除非你不想要哪些全封装的第三方hooks api,以及不想要更简单明了的

但是效率和质量是可以做权衡的,这部分看你怎么看,不过一旦你全使用 useReducer 进行开发,就是默认了,有 useReducer 处,就是个单独的模块

也就是说,要么全用 useReducer,要么只用 useReducer 做模块通讯(其他调度系统的通讯也可以看做模块通讯)

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享