当前位置: 首页 > 科技观察

十分钟教你手写九个常用的自定义Hooks

时间:2023-03-16 01:21:28 科技观察

前言Hook是React16.8的新特性。它让您无需编写类即可使用状态和其他React功能。本文是一篇以实战为主的文章。主要讲解hooks的使用方法以及实际项目中的一些最佳实践。就不一步步介绍reacthooks的由来和基本使用了,因为关于hooks的文章很多,官网对于reacthooks的介绍也很详细,不熟悉的可以可以看官网。正文1.Reacthooks核心API使用注意事项笔者在项目中常用的hooks主要有useState、useEffect、useCallback、useMemo、useRef。当然像useReducer,useContext,createContext这样的hook在H5游戏中也会用到,因为不需要维护复杂的状态,所以我们可以通过上面三个API构建自己的小redux(后面会介绍小redux的实现方法稍后)要处理全局状态,但是对于复杂的企业项目,我们使用redux及其生态会更高效。当我们使用钩子和函数组件来编写我们的组件时,首先要考虑的是渲染性能。我们知道如果我们在函数组件中使用setState而不做任何处理,会导致组件内部重新渲染。一个比较典型的场景:当我们手动更新容器组件中的任何状态时,容器内部的每个子组件都会被重新渲染。为了避免这种情况,我们通常使用memo来包裹函数组件,达到类组件的pureComponent效果:importReact,{memo,useState,useEffect}from'react'constA=(props)=>{console.log('A1')useEffect(()=>{console.log('A2')})return

A
}constB=memo((props)=>{console.log('B1')useEffect(()=>{console.log('B2')})return
B
})constHome=(props)=>{const[a,setA]=useState(0)useEffect(()=>{console.log('start')setA(1)},[])return
}当我们换行Bwithmemo,状态a的更新不会导致B组件重新渲染。其实仅仅优化这一点是不够的。例如,如果我们的子组件使用了容器组件的某个变量或函数,那么当容器内部的状态更新时,这些变量和函数会被重新赋值,这将导致即使子组件组件仍然会被重新渲染即使使用了memo包,所以这时候我们就需要使用useMemo和useCallback。useMemo可以帮我们缓存变量,useCallback可以缓存回调函数。他们的第二个参数和useEffect一样,都是一个依赖数组。通过配置依赖数组来决定是否更新。importReact,{memo,useState,useEffect,useMemo}from'react'constHome=(props)=>{const[a,setA]=useState(0)const[b,setB]=useState(0)useEffect(()=>{setA(1)},[])constadd=useCallback(()=>{console.log('b',b)},[b])constname=useMemo(()=>{returnb+'xuxi'},[b])return
}此时组件B为更新后不会再次重新渲染。以上优化步骤主要用于优化组件的渲染性能。我们通常还会涉及获取组件DOM和使用内部闭包变量的场景。这时候我们就可以使用useRef了。useRef返回一个可变的ref对象,其.current属性被初始化为传递的参数(initialValue)。返回的ref对象在组件的整个生命周期中持续存在。函数AutoFocusIpt(){constinputEl=useRef(null);constuseEffect(()=>{//`current`指向挂载的文本输入元素inputEl.current.focus();},[]);return(<>);}除了上面的应用场景,我们还可以用它来实现类组件的setState功能。介绍。2.实现一个小的redux要实现redux,我们会用到前面提到的三个api,useReducer、useContext、createContext。至于如何实现redux,其实网上有很多实现方法。这里我写了一个demo供大家参考://actionType.jsconstactionType={INSREMENT:'INSREMENT',DECREMENT:'DECREMENT',RESET:'RESET'}exportdefaultactionType//actions.jsimportactionTypefrom'./actionType'constadd=(num)=>({type:actionType.INSREMENT,payload:num})constdec=(num)=>({type:actionType.DECREMENT,payload:num})constgetList=(data)=>({type:actionType.GETLIST,payload:data})export{add,dec,getList}//reducer.jsfunctioninit(initialCount){return{count:initialCount,total:10,user:{},article:[]}}}functionreducer(state,action){switch(action.type){caseactionType.INSREMENT:return{count:state.count+action.payload};caseactionType.DECREMENT:return{count:state.count-action.payload};caseactionType.RESET:returninit(action.payload);默认值:抛出新的错误();}}export{init,reducer}//redux.jsimportReact,{useReducer,useContext,createContext}from'react'import{init,reducer}from'./reducer'constContext=createContext()constProvider=(props)复制代码=>{const[state,dispatch]=useReducer(reducer,props.initialState||0,init);return({props.children})}export{Context,Provider}其实还有更优雅的实现方式。作者之前也写过几套redux模板。欢迎一起讨论。接下来进入正文,带大家实现几个常用的自定义hook。3、实现自定义useState,支持类似类组件的setState方法熟悉react的朋友都知道,当我们使用类组件更新状态时,setState会支持两个参数,一个是更新后的状态或者回调更新的状态,和另外一个参数是updated回调函数,用法如下:this.setState({num:1},()=>{console.log('updated')}),但是useState的第二个参数回调hooks函数的支持类似于类组件setState第一个参数的使用,不支持第二个参数callback,但是在很多业务场景中,我们希望hooks组件能够支持更新后的回调方法,那么应该怎么办呢?我们的确是?其实问题也很简单,只要我们非常清楚hooks和api的原理,我们就可以通过自定义hooks来实现。这里我们使用上面提到的useRef和useEffect配合useState来实现这个功能。注意:reacthooks的useState必须放在函数组件的最顶层,不能写在ifelse等条件语句中,以保证hooks的执行顺序一致,因为useState底层是通过一个链接实现的列表结构,具有严格的顺序。先看一下实现的代码:=(state,cb)=>{setState(prev=>{isUpdate.current=cbreturntypeofstate==='function'?state(prev):state})}useEffect(()=>{if(isUpdate.current){isUpdate.current()}})return[state,setXState]}exportdefaultuseXState作者利用useRef的特性作为标识来区分挂载还是更新。执行setXstate时,会传入和setState一模一样的参数,并将回调赋值给useRef的current属性,这样当更新完成后,我们可以手动调用current来实现更新回调的功能。是不是很聪明?4.实现自定义useDebounce节流功能和防抖功能想必大家都不陌生吧。为了让我们在开发中更优雅的使用节流防抖功能,我们往往需要让某个状态也具有节流防抖功能,或者调用某个函数。为了避免频繁调用,我们很多时候也会采用节流防抖的思路。原来的节流防抖功能可能如下代码所示://Throttlefunctionthrottle(func,ms){letprevious=0;returnfunction(){让现在=Date.now();让上下文=这个;让args=参数;如果(现在-以前>ms){func.apply(context,args);以前=现在;}}}//去抖函数debounce(func,ms){lettimeout;返回函数(){让上下文=this;让args=参数;如果(超时)clearTimeout(超时);timeout=setTimeout(()=>{func.apply(context,args)},ms);}}那我们先实现防抖钩子,代码如下:import{useEffect,useRef}from'react'constuseDebounce=(fn,ms=30,deps=[])=>{lettimeout=useRef()useEffect(()=>{if(timeout.current)clearTimeout(timeout.current)timeout.current=setTimeout(()=>{fn()},ms)},deps)constcancel=()=>{clearTimeout(timeout.current)timeout=null}return[cancel]}通过代码导出默认的useDebounce可知useDebounce接受三个参数,分别是回调函数、时间间隔和依赖数组。对外暴露了cancelAPI,主要用于控制何时停止debounce功能具体用法如下://...import{useDebounce}from'hooks'constHome=(props)=>{const[a,setA]=useState(0)const[b,setB]=useState(0)const[cancel]=useDebounce(()=>{setB(a)},2000,[a])constchangeIpt=(e)=>{setA(e.target.value)}return
<输入类型="text"onChange={changeIpt}/>{b}{a}
}上面的代码实现了statedebounce的功能,具体效果如下图所示:实现throttlinghooks功能。直接上代码:import{useEffect,useRef,useState}from'react'constuseThrottle=(fn,ms=30,deps=[])=>{letprevious=useRef(0)let[time,setTime]=useState(ms)useEffect(()=>{letnow=Date.now();if(now-previous.current>time){fn();previous.current=now;}},deps)constcancel=()=>{setTime(0)}return[cancel]}exportdefaultuseThrottle代码和自定义useDebounce类似,但是需要注意一点,为了实现取消功能,我们使用了内部状态进行处理,并且通过控制时间间隔来取消节流效果。当然还有很多其他的方法可以实现这个hooksAPI。具体效果如下:6.实现自定义useTitle自定义useTitlehooks其实有很多使用场景,因为我们现在的项目大部分都是采用SPA或者hybridSPA的方式开发的。对于不同的路由,我们也希望和多页面应用一样,能够切换到相应的标题,让用户更好的了解页面的主题和内容。这个hooks的实现也很简单,我们直接上代码:import{useEffect}from'react'constuseTitle=(title)=>{useEffect(()=>{document.title=title},[])return}exportdefaultuseTitle上面的代码表明我们只需要在useEffect中设置文档的title属性即可,不需要返回任何值。其实还有更优雅复杂的实现方式,这里就不举例了。具体使用如下:constHome=()=>{//...useTitle('Interestingfront-end')return
home
}7.实现自定义useUpdate我们都知道如果我们想让组件重新渲染,就得更新状态,但是有时候业务需要的状态是没有必要更新的,我们不能为了让组件重新渲染就强行无意义的更新一个状态,所以这时候我们可以自定义一个Updatehooks来优雅的实现组件的强制更新。实现代码如下:import{useState}from'react'constuseUpdate=()=>{const[,setFlag]=useState()constupdate=()=>{setFlag(Date.now())}returnupdate}exportdefaultuseUpdate上面的代码可以发现我们的useUpdatehook返回了一个函数,这个函数是用来强制更新的。使用方法如下:constHome=(props)=>{//...constupdate=useUpdate()return
{Date.now()}
update
}效果如下:8.实现自定义useScroll自定义useScroll也是经常出现的问题之一。我们经常监视元素滚动位置的变化,以确定要显示的内容。该应用场景广泛应用于H5游戏开发。接下来我们看一下实现代码:import{useState,useEffect}from'react'constuseScroll=(scrollRef)=>{const[pos,setPos]=useState([0,0])useEffect(()=>{函数handleScroll(e){setPos([scrollRef.current.scrollLeft,scrollRef.current.scrollTop])}scrollRef.current.addEventListener('scroll',handleScroll,false)return()=>{scrollRef.current.removeEventListener('scroll',handleScroll,false)}},[])returnpos}exportdefaultuseScroll从上面的代码可以看出,我们需要在钩子函数中传入一个元素的引用,这个我们可以通过在函数组件中使用ref和useRef获得。该钩子返回滚动的x,y值,即滚动的左位移和上位移。具体用法如下:importReact,{useRef}from'react'import{useScroll}from'hooks'constHome=(props)=>;{constscrollRef=useRef(null)const[x,y]=useScroll(scrollRef)返回
{x},{y}
}通过useScroll,这个钩子会帮我们自动监听容器滚动条的变化,实时获取滚动位置。具体效果如下:9、实现自定义useMouse与useScroll类似,自定义useMouse和createBreakpoint的实现方法与自定义createBreakpoint的实现类似。它们都是通过监听window或者dom事件来自动更新我们需要的值。这里我就不一一实现了。如果你不明白,你可以用它和我交流。通过这些自定义的钩子,可以大大提高我们代码的开发效率,重复的代码也可以得到有效的复用,让大家在工作中多尝试。当我们写了很多自定义的hooks时,一个好的开发体验就是统一管理和分发这些hooks。作者建议我们可以在项目中单独创建一个hooks目录来存放这些可复用的hooks,方便管理和维护。如下: