上文有提到,useReducer 的 dispatch 实际上可以看成,一个封闭系统内,两个子系统融合,保证其中一个系统低熵环境的 “麦克斯韦妖”,这么说可能有点拗口,但是重点放在 维持低熵 (低状态数量,低状态变化数量)上即可,不必过多纠结其他概念
那么,低状态数量,地状态变化数量,意味着什么呢?我们先分析一个例子:
给定两个状态 a,b ,在 1000ms 无状态变化后,输出最近变化过的 a/b 值
如果是极限函数式+单向流的思维,着重点在 1000ms无变化,最近变化这些字眼上,即组合 debounce ,merge ,很快得到需要的结果,甚至代码以字母数计:
const result$ = merge(a$,b$).pipe(debounceTime(1000))
复制代码
这似乎是个毫不费力的工作,对么?
不一定,原因很简单,因为很多实现被隐藏了 —— 调度被隐藏,数据关系被隐藏,发起者接受者关系被抽象,整个系统逻辑结构被隐藏
对于个人代码或者成熟代码再封装,这种代码毫无疑问是非常优秀的,但是对于一个正在更新迭代的工程项目,这种“炫技”毫无意义 —— 你确定自己这么做不会玩脱?
我们来看看这个极限函数式代码(更极限的话,等于号都不应该有),有多少个抽象层级:
-
响应式数据抽象:将数据以及所有的变化,抽象成一个结构(类似 vue 的 ref,react 未拆开的 state)
-
调度抽象:将调度抽象成流,多个调度之间的关系抽象成流之间的逻辑(也就是子弹图)
一共有两层抽象,这能说是好事么?一层抽象已经让沟通交流加大了困难,两层抽象明显不符合工程化的需求,当然,上面所举的例子比较简单,如果是 ng 等事件调度(zone 调度)技术栈,这也是最佳实践(一定要保证这种抽象层次团队成员能够接受),即便如此,复杂的逻辑依旧不推荐流,因为工程不是一个人的事(是老板的事)
那好,我们用数据驱动的状态,来做一下这个逻辑,只做 hooks 下的响应式抽象(state hooks + effect hooks)
const [a,setA] = useState('')
const [b,setB] = useState('')
const [result,setResult] = useState('')
useEffect(()=>{
setTimeout(()=>{
// ...
},1000)
},[a,b])
复制代码
等等!突然出现了两个问题让人寸步难行!
-
setTimeout 是 react 调度外事件,它的回调也需要附加依赖,否则无法获取最新的变更
-
我们需要拿到最新变化过的值,因此,我们需要知道 a,b 的变化时间,很明显,只有 a,b,result 三个状态不够
-
虽然 effect 的发起者是 a,b,但是 setTimeout 中还隐含了一个变化时间的数据,没有它我们无法确定 a,b 变化情况
const [a,setA] = useState('')
const [b,setB] = useState('')
const [result,setResult] = useState('')
// a,b 皆变化的时间
const [abEffect,setAbEffect] = useState<Date,undefined>()
useEffect(()=>{
setAbEffect(new Date())
},[a,b])
// 调度数据
const [aTimeout,setATiemout] = useState<[Date,string]|undefined>()
const [aTimeout,setATiemout] = useState<[Date,string]|undefined>()
// a 变化记录时间
const handleATimeout = useCallback(()=>{
// 记下 a 的值
setATimeout([new Date(),a])
},[a])
useEffect(()=>{
setTimeout(handleATimeout,1000)
},[handleATimeout])
// b 变化记录时间
const handleBTimeout = useCallback(()=>{
setBTimeout([new Date(),b])
},[b])
useEffect(()=>{
setTimeout(handleBTimeout,1000)
},[handleBTimeout])
// 在 a,b 变化时间下,求得最近变化的 a,b
useEffect(()=>{
if(aTimeout && bTimeout){
const [aT,a] = aTimeout
const [bT,b] = bTimeout
const abT = aT
const result = a
if(aT.getTime()-bT.getTime()>0){
abT = aT
result = a
}else{
abT = bT
result = b
}
if(abT.getTime()-abEffect.getTime()>1000){
setResult(result)
}
}
},[aTimeout,bTimeout,abEffect])
复制代码
看到上面这一串,你会发现,这段代码乱的一批的同时,性能还贼差!
为什么?原因就是出现在,react 本身感知不到合成事件外的事件
即 timeout,websocket,webworker,fetch,mediastream 等等,为了保证数据一致性,他们必须在 useEffect 中调用,并保证依赖传递
就是基于这个原因,所以这些事件的处理,也必须数据驱动,也就是你的眼里没有异步,没有事件,纯粹从数据状态的角度思考和实现,这回带来巨大的性能消耗+冗长的代码量
注意,仅仅是在出现非 react 感知事件,也就是无法触发 react dispatchEvent 的事件如此,换句话说,也就是 React 管理的 synthedicEvent->effect->setState->useMemo->jsx 过程之外,使用纯数据驱动会带来巨大的负担,并不意味着普通 React MVI 模型也会如此糟心,相反,没有 异步 的 hooks 代码高效到可怕
我们会发现,这种遇到不确定事件,不确定系统时,巨大熵增必然导致数据膨胀(即 a,b,result 三个量完全不够,熵增就是数据膨胀和数据可能性变化的膨胀)
而所谓 aTimeout,bTimeout,abEffect 等数据,都属于 timeout 系统的熵,并不属于当前组件代码
所以,这个时候,我们应该将 useReducer 里面的麦克斯韦小精灵拿出来,将 aTimeout,bTimeout,abEffect 等数据锁死在 useReducer 结构中,保证外部结构的低熵环境
const [state,dispatch] = useReducer((state,[type,payload])=>{
// 回调调用
if(type==='change'){
const now = new Date()
if(now.getTime()-state.triggerTime.getTime()>1000){
return {...state, result: payload}
}
}
// api 调用
if(type==='trigger'){
return {...state, triggerTime: new Date()}
}
return state
},undefined,()=>({result:'',triggerTime:new Date()}))
const handleA = useCallback(()=>{
dispatch(['change',a])
},[a])
useEffect(()=>{
dispatch('trigger')
setTimeout(handleA)
},[handleA])
const handleB = useCallback(()=>{
dispatch(['change',b])
},[b])
useEffect(()=>{
dispatch('trigger')
setTimeout(handleB)
},[handleB])
复制代码
注意,useReducer 是状态机,它并不适合处理异步,本身没有异步能力
或许能用 thunk 赋予它能力?也不合理,因为异步逻辑事件驱动,在调度处处理永远是最合适的,useEffect 是响应式调度处理中心,轮不到 useReducer 来处理异步,这点与 redux 不同(redux 是实在没有办法)
说道 thunk,thunk 的状态好像也可以自己实现一个 reducer:
const [action,setAction] = useState(()=>()=>['default',undefined])
useEffect(()=>{
const [type, payload] = action()
if(type === 'init'){
// reducer
}
},[action])
复制代码
这算是一个简易的 reducer 方案,setAction 的类型也是 dispatch,但是最大的问题是,它会按照 react 调度进行事件分发
,即:
-
事件延迟调度
-
同步事件被合并
这些在绝大部分事件调用中不会成为问题,但是在极个别即时性,并发事件(同步并发)场景下,用 useReducer 会更好一些
useMemo 是状态的 memo, useReducer 是事件的 memo
是的,综上所述,如果把 React 开发看成一个控熵(开发本就是控熵过程)的过程,那么 useMemo 就是针对 React 主流程进行熵控制,即减少状态可能性
而 useReducer 更像是事件 memo,即减少事件的熵