当前位置: 首页 > Web前端 > HTML

大家看得懂的源码——ahooks这些hooks更优雅的管理你的状态

时间:2023-03-28 16:38:06 HTML

本文是深入浅出的ahooks源码系列文章的第十二篇,整理成文档地址了。觉得还不错,给个star支持一下,谢谢。今天我们就来聊聊那些可以帮助我们更优雅地管理状态的hooksinhooks。一些比较特殊的,比如cookie/localStorage/sessionStorage、useUrlState等,已经单独详细讨论过了。有兴趣的可以看看作者的历史文章。useSetState管理对象类型state的Hooks,用法和类组件的this.setState基本一致。我们先来了解可变数据和不可变数据的含义和区别如下:可变数据是指一个数据创建后,可以随时修改,修改后会影响到原来的值。不可变数据是一旦创建就无法更改的数据。对Immutable对象的任何修改或添加或删除都会返回一个新的Immutable对象。我们知道React函数组件中的State是不可变数据。所以我们经常需要写类似下面的代码:setObj((prev)=>({...prev,name:'Gopal',others:{...prev.others,age:'27',}}));通过useSetState,可以省略对象扩展操作符操作这一步,即:setObj((prev)=>({name:'Gopal',others:{age:'27',}}));它的内部实现也比较简单,如下图:在调用设置值方法时,会根据传入的值是否为函数来判断。如果是函数,输入参数是旧状态,输出是新状态。否则,直接作为新状态。这符合setState的使用方法。使用对象扩展操作符返回一个新对象,保证原始数据不可变。constuseSetState=>(initialState:S|(()=>S),):[S,SetState]=>{const[state,setState]=useState(初始状态);//合并操作并返回一个新值constsetMergeState=useCallback((patch)=>{setState((prevState)=>{//新状态constnewState=isFunction(patch)?patch(prevState):patch;//它也可以通过类似Object.assign的方式进行合并//对象扩展运算符返回一个新对象,保证原始数据不可变returnnewState?{...prevState,...newState}:prevState;});},[]);返回[状态,setMergeState];};可以看到,其实它里面封装了对象扩展运算符的操作。还有其他更优雅的方式吗?我们可以使用与useState非常相似的use-immeruseImmer(initialState)。该函数返回一个元组,其第一个值是当前状态,第二个是更新函数,它接受一个生成器函数或一个值作为参数。使用如下:const[person,updatePerson]=useImmer({name:"Michel",age:33});functionupdateName(name){updatePerson(draft=>{draft.name=name;});}functionbecomeOlder(){updatePerson(draft=>{draft.age++;});}当传递一个函数给update函数时,draft参数可以自由更改,直到producer函数结束,所做的更改将不可变并成为next状态。这样比较符合我们的使用习惯,我们对象的值可以通过draft.xx.yy来更新。useBoolean和useToggle都是特例值管理。useBoolean,一个用于管理布尔状态的优雅Hook。useToggle,一个用于在两个状态值之间切换的Hook。其实useBoolean是useToggle的一种特殊使用场景。先看useToggle。这里使用typescript函数重载来声明输入输出参数类型,根据不同的输入参数返回不同的结果。例如,如果第一个输入参数是布尔值,则返回一个元组,第一项是布尔值,第二项是更新函数。优先级从上到下变低。输入参数可能有两个值,第一个是默认值(认为是左值),第二个是求反后的值(认为是右值),不能传,传不传会直接根据默认值取反值!defaultValue。开关功能。切换值,即上述左值和右值的转换。放。直接设置值。置左。设置默认值(左值)。设置正确。如果传入reverseValue,则设置为reverseValue。否则设置为defautValue的取反值。//TS函数重载functionuseToggle():[boolean,Actions];functionuseToggle(defaultValue:T):[T,Actions];functionuseToggle(默认值:T,反向值:U):[T|U,动作];functionuseToggle(//默认值defaultValue:D=falseasunknownasD,//取反reverseValue?:R,){const[state,setState]=useState(默认值);constactions=useMemo(()=>{constreverseValueOrigin=(reverseValue===undefined?!defaultValue:reverseValue)asD|R;//切换状态consttoggle=()=>setState((s)=>(s===defaultValue?reverseValueOrigin:defaultValue));//修改状态constset=(value:D|R)=>setState(value);//设置为默认值constsetLeft=()=>setState(defaultValue);//如果传入reverseValue,则设置为reverseValue。否则设置为defautValue的反向值constsetRight=()=>setState(reverseValueOrigin);返回{toggle,set,setLeft,setRight,};//useToggle忽略值变化//},[defaultValue,reverseValue]);},[]);return[state,actions];}而useBoolean是对useToggle的使用。如下,比较简单,不细说了exportdefaultfunctionuseBoolean(defaultValue=false):[boolean,Actions]{const[state,{toggle,set}]=useToggle(defaultValue);constactions:Actions=useMemo(()=>{constsetTrue=()=>set(true);constsetFalse=()=>set(false);return{toggle,set:(v)=>set(!!v),setTrue,setFalse,};},[]);返回[状态,动作];}usePreviousHook保存最后的状态。原理是每次状态变化时比较值是否变化,改变状态:保持两个状态prevRef(保存上一个状态)和curRef(保存当前状态)。当状态发生变化时,使用shouldUpdate判断是否有变化,默认是通过Object.is判断。开发人员可以自定义shouldUpdate函数并决定何时记录最后的状态。当状态改变时,将prevRef的值更新为之前的curRef,将curRef更新为当前状态。constdefaultShouldUpdate=(a?:T,b?:T)=>!Object.is(a,b);函数usePrevious(state:T,shouldUpdate:ShouldUpdateFunc=defaultShouldUpdate,):吨|undefined{//利用useRef的特性,保持引用不变//保存最后一个值constprevRef=useRef();//当前值constcurRef=useRef();//from定义是否更新最后一个值if(shouldUpdate(curRef.current,state)){prevRef.current=curRef.current;curRef.current=状态;}返回prevRef.current;}useRafState只在requestAnimationFrame回调时更新状态,一般用于性能优化。window.requestAnimationFrame()告诉浏览器——你要执行一个动画,要求浏览器在下次重绘前调用指定的回调函数来更新动画。该方法需要传入一个回调函数作为参数,回调函数会在下次浏览器重绘前执行。如果你的操作比较频繁,可以使用这个钩子来进行性能优化。关注setRafState方法。当它被执行时,它会取消最后的setRafState操作。再利用requestAnimationFrame来控制setState的执行时机。另外,当页面卸载时,会直接取消操作,避免内存泄漏。函数useRafState(initialState?:S|(()=>S)){constref=useRef(0);const[state,setState]=useState(initialState);constsetRafState=useCallback((value:S|((prevState:S)=>S))=>{cancelAnimationFrame(ref.current);ref.current=requestAnimationFrame(()=>{setState(value);});},[]);//unMount的这个时候去掉监听useUnmount(()=>{cancelAnimationFrame(ref.current);});return[state,setRafState]asconst;}useSafeState用法与React.useState完全相同,但是组件卸载后异步回调中的setState不再执行,避免组件卸载后更新状态导致内存泄漏。代码如下:更新时使用useUnmountedRef判断组件是否卸载,然后停止更新。functionuseSafeState(initialState?:S|(()=>S)){//判断是否卸载constunmountedRef=useUnmountedRef();const[state,setState]=useState(initialState);constsetCurrentState=useCallback((currentState)=>{//如果组件被卸载,停止更新if(unmountedRef.current)return;setState(currentState);},[]);return[state,setCurrentState]asconst;}useUnmountedRef之前我们提到过这个,简单回顾一下,其实就是在钩子的返回值中将组件标记为unloaded。constuseUnmountedRef=()=>{constunmountedRef=useRef(false);useEffect(()=>{unmountedRef.current=false;//如果已经卸载,则执行return中的逻辑return()=>{unmountedRef.current=true;};},[]);returnunmountedRef;};useGetState在React.useState中增加了一个getter方法来获取当前的最新值。它的实现是这样的:其实就是通过useRef记录最新的state值,对外暴露一个getState方法来获取最新的。函数useGetState(initialState?:S){const[state,setState]=useState(initialState);//useRef返回一个可变的ref对象,其.current属性被初始化为传递的参数(initialValue)。返回的ref对象在组件的整个生命周期中持续存在。//使用useRef处理状态conststateRef=useRef(state);stateRef.current=状态;constgetState=useCallback(()=>stateRef.current,[]);返回[状态,设置状态,获取状态];在某些情况下,可以避免React的闭包陷阱。如官网例子:const[count,setCount,getCount]=useGetState(0);useEffect(()=>{constinterval=setInterval(()=>{console.log('intervalcount',getCount());},3000);return()=>{clearInterval(interval);};},[]);如果这里没有使用getCount(),而是直接使用count,则无法获取到最新的值。总结与思考React的函数Component的状态管理还是比较灵活的。我们可以对一些场景进行封装和优化,从而更加优雅的管理我们的状态。希望ahooks的封装对大家有所帮助。本文已收录在我的个人博客中,欢迎关注~