React 你知道 useMemo,useCallback 和 useEffect,useLayoutEffect 的依赖数组,含义不同么?
任何逻辑,都可以拆分成这样的模型:
事件引起状态的变化,状态的变化又发起新的事件
“当 xxxx 发生时,xxxx 的数据会发生改变”,事件在前,状态在后,它是什么样的形式呢?
引用 cycle.js 的说明文档,事件到状态的依赖箭头,在事件那里
即 事件主动发起,状态被动承受,注意这个主被动关系
但是,我们换个描述
“监听到 xxxx 数据变化时,执行 xxxx”,状态在前,事件在后,它就会变成这种形式:
如图,依赖关系在 Bar,状态所有者,可以 主动控制 流程的执行
这种主被动的转换,就是 响应式编程 的核心
那么我们来看看 React 那些带依赖数组的 api,哪些是主动的?而哪些又是被动的呢?
useEffect,useLayoutEffect 是主动 API
useEffect(()=>{
if(a){
// ...
}
},[a,b,c,d])
控制结构在 useEffect 中,所以 useEffect 是主动的,是 响应式 API
这就意味着,无论这个结构在哪里,它都能实现独立自主,也就是实现了 低耦合,同时融合了 React 的调度机制,在处理异步方面也有天然的优势
useMemo,useCallback,甚至 useState 是 被动 API
useCallback 无法决定何事变化, useMemo 只是跟随 state,而 useState 也无法决定自己如何变化
useMemo 无所谓,因为数据不会自发地触发事件
但是,当你在 useEffect 中使用 useCallback 的返回值时,巨大的陷阱就出现了:
const cb = useCallback(()=>{/* ... */},[/* ... */])
useEffect(()=>{
// 耦合 cb,如果 cb 不在本作用域内声明,你将很难确定依赖,控制调度
},[cb])
于是会有很多神奇的死循环 effect 诞生,既 effect(主动)->callback(被动?主动?)
useEffect 中的依赖数组,是主动的,在 effect 中作为事件发起者,应该作用于 useState 的 setter,而这里却写入了一个 被动控制的 useCallback,一旦 callback 的依赖与 effect 耦合,就会出现回环,死循环诞生
const cb = useCallback(()=>{
setA('') // 这里写入 a
},[a,b,c,d])
useEffect(()=>{
},[cb,a]) // 这里传入 a
如果需要避免出现回环,很简单 ——
尽量不要在 useEffect 中调用 带依赖callback
如何保证无依赖 callback 呢?
很简单,我们将所有这些 事件->状态->事件->状态
的流程,都找出来,然后将它变换成:
不就可以了么?
MVI 模型
我们将 IO 进行高度抽象,不难发现,只是一个如此的流程:
而其中,处理输入部分,设计为 事件,而输出部分,设计为 状态,计算部分,设计为 state + effect 集合
则可以轻松实现建模,而这个模式,被称作 MVI 模型:
其中 intent 部分,就是 useCallback 发力的地方,既 ——
我们用 useCallback 将 事件转化为状态
那你需要为 useCallback 添加依赖么?
const [a,setA] = useState()
const [b,setB] = useState()
const intentCb = useCallback(()=>{
setA('')
setB('')
},[])
基于这个目的的 useCallback 使用,不会带有任何依赖,可以放心地进行传递,组合:
const refresh = useCallback(()=>{},[])
useEffect(
()=>{
refresh()
}
,[refresh,a,b,c,d,e])
既:
容易被人忽略的一点,是 view 层
MVI 中,用 DOM SINK 描述 view 层,即从调度上来讲,不是每个用户操作,都会让每个数据进行变化,因此,需要做一次类似过滤一样的工作
但是,开发者们总是忘记这一部分,比如代码中很容易习惯性直接 return 一个 element
function SomeCompo(){
// ...
return <div>
{/* state 好说,变换容易控制,callback 的变化很难控制,因为 function 总是新值 */}
<SomeExpenssiveCompo value={[state,someCb]}/>
<div>
}
React 官方解法是 用 useReducer 向下传递回调,并分开写 context
const [state,dispatch] = useReducer(()=>{},{})
return <State.Provider value={state}>
<Dispatcher.Provider value={dispatch}>
{/* ... */}
</Dispatch.Provider>
</State.Provider>
我认为这很蠢!且不说原本属于同一上下文逻辑的的部分需要拆开,写成嵌套的上下文,让依赖注入结构更加复杂
最主要的是, useReducer 很难用上各种第三方 Hooks 生态,比如 react-use,swr 等,本身异步支持也不够
当然,有模块能力的 useReducer 都如此,更不要说全局统一上下文,无拆分初始化能力的 Redux 等统一状态管理库了
这里的问题,我认为真正的解法是 —— 头疼医头脚疼医脚
-
无依赖的 callback 替代 dispatch (本来就是无依赖,不会变化)
-
为 jsx 加上 useMemo
是的,你的目的就是对组件调度进行控制,既 DOM sink(沉降)
你应该直接作用于消费阶段(JSX Element)(将这个逻辑提前到 model 阶段,是个非常傻的做法)
function SomeCompo(){
return <div>
{/* 只在 a,b,c 变化时,刷新这个 component */}
useMemo(()=> <SomeExpenssiveCompo value={[state,someCb]}/>, [a,b,c])
<div>
}
很简单就能解决的问题,为何要上 useReducer 呢?
我个人非常反对在 js/ts 中使用 reducer 这种状态机制,js/ts 没有模式识别,没有完善的协变类型,很多时候是靠开发者自己来控制代码行文,用字符串模拟协变类型,用if/switch 语句强行模式识别,这样的函数式状态机,真的没有存在的必要
当然,我反对在 js/ts 中使用 reducer,并不影响我认为它在 reasonml 中很美~
不过我没有 flow + react 的开发经历,或许校验类型系统能够给这种开发方式带来更好的体验,但是基于 js/ts 的环境,还是不报太大希望
没错,大家注意到,我在说明 主被动/响应式编程 和 MVI 模型的时候,都是用的 cycle.js 的文档,而 cycle.js 又是基于或者借鉴 Rxjs 的
没错,这种基于 MVI 模型 + 主被动响应式开发的思想,就是 ——
-
流程上,将一次 IO 抽象为 intent,model,view,intent 发起,model 处理,view 沉降
-
逻辑上,区分主被动响应模式,如果全部更改为主动模式,即是 事件驱动流 (rx,xstream),如果全部切换为被动模式,即是 数据驱动流(react主要模式)
既 事件 -> 事件 -> 事件
和 数据 -> 数据 -> 数据
换句话说,如果你的应用逻辑部分主要采用 useCallback + state 的模式,就是事件驱动模型(全员被动),如果你的应用逻辑部分主要采用 useEffect + state 的模式,就是数据驱动模型(全员主动)
React 更加适合 数据驱动模型,为何?
哈哈哈,因为 React 没有将事件进行完全代理啊(用过 ng zone 的同学,可以站出来,科普一下何为完全暴力事件代理)
换言之,并非所有的事件,react 都能够感知到,合成事件处,你可以直接调用 useCallback,非合成事件呢?(setTimeout,promise,socket,web worker,media stream?)
你可能需要在 useEffect 中 调用 useCallback 了吧?
const handleTimeout = useCallback(()=>{
// timeout
},[])
useEffect(()=>{
setTimeout(handleTimeout)
},[handleTimeout])
每次这些事件的处理,都必须小心翼翼,胆战新机
不过,有一种特殊的模式,可以用在这些地方:
action 模式 - 按照 React 调度执行函数
const [action,disaptch] = useState(()=>()=>'')
useEffect(()=>{
// 函数参数
const params = action()
// 函数逻辑
},[action])
// 在某一处
dispatch(()=>'new param')
原理很简单,只是用 useEffect 的写法,来写一个 useCallback,不过,这个所谓函数的调度,完全是由 react 控制,即:
- 异步进行调用
- 同一事件循环,只会调用一次
第二个问题可以使用参数集合的形式解决,而第一个问题,基本误解(useCallback 的话,你也无解,useEffect 处理逻辑必须按照 react 调度进行,毕竟这也是没有办法的事)
这样的话,setTimout 等异步事件,就非常好解决了
const [action,dispatch] = useState(()=>()=>{})
useEffect(()=>{
console.log('timeout')
},[action])
useEffect(()=>{
setTimeout(()=>{ dispatch(()=>{}) })
},[])
数据驱动 - 笨办法,解决复杂问题
数据驱动没有像事件驱动那样,有那么多的超高度抽象的工具,比如 switchMap,merge,combine,debounce(是的,数据驱动你甚至不用 debounce,一个 timer + 另一个标识即可),但是他却高效稳定得可怕
不难理解为何很多 hooks 工具,会这样封装:
/* swr */ const {data,error,loading} = useSwr(key,fetcher)
`/* react-use */ const [state, doFetch] = useAsyncFn(async ()=>{},[])
但是,这里就不得不说 React 的一大缺点了,即 ——
数据驱动,并不适合函数式
没错,数据驱动并不适合函数式
- 一个函数调用有调用和返回值两个状态,还有调用次数和错误,同步多次调用的话,还有参数集合等数据
- 一个异步函数调用,除了函数都有的状态,还有loading,为了方便调度控制,还有历史 loading 时间,被调用时间,发起时间以及他们的列表 等
- 一个不断触发的事件,更是有数不清的数据和相关的描述信息
这让状态数量极具扩张,因此,如何管理状态,就是一个非常棘手的问题
用函数加返回值?既函数模拟类(无继承,继承本身应该被抛弃),效果非常差 —— 没有附加描述信息,没有自解释性
应该用类似 Golang interface 或者 直接使用 贫血类 的方式,来管理封装状态
而 React 这方面的支持,不能说没有,只能说很差(要知道 class 有自解释性,对于庞大的 state 集合,没有自动生成文档,自动生成图形统计,这些 state 只能让你的开发体验直线下降)
再加上 React 并没有完整的事件驱动支持(即没有代理全部事件,保证用户代码在 model - view 范畴内,实现全被动),你如果采用 事件驱动 Rxjs 等工具辅助开发,体验也不会好(思维负担加重)
这算是 React 最大的缺点吧,不过原因也很正常,reason react 才是 react 嘛,哈哈哈
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!