写在前面ReactHooks是React团队在两年前的16.8版本中引入的全新机制。作为最主流的前端框架,React的API非常稳定。此次更新的发布,震惊了很多怕新轮子的前端大佬们。毕竟每次更新都是一次高成本的学习。什么?答案很容易使用。对于React开发者来说,它只是多了一种选择。以前的开发方式是基于Class组件,而hooks是基于函数组件,这意味着两种开发方式可以共存,新的代码可以根据具体情况使用Hooks来实现。本文主要介绍Hooks的优点和几个常用的钩子函数。Hooks的优点1.不足类组件代码量大:相对于函数组件的写法,使用类组件需要多一点代码,这是最直观的感受。this指向:this的指向在类组件中总是需要考虑的,但是在函数组件中可以忽略。趋于复杂和难以维护:在较高版本的React中,更新了一些生命周期函数。因为这些功能相互解耦,容易造成写的分散、不集中,漏掉关键逻辑,增加冗余逻辑,导致后期调试。困难。相反,hooks可以把所有的关键逻辑放在一起,不那么零散,调试的时候更容易理解。状态逻辑复用困难:组件之间状态逻辑复用困难。Renderprops(渲染属性)或者HOC(高阶组件)都可能用到,但是不管是渲染属性还是高阶组件,都会在原来的组件包裹一层父容器(一般是div元素),导致分层冗余。2.Hooks带来的好处逻辑复用要复用组件前的状态逻辑,往往需要高层组件等复杂的设计模式。这些高层组件会产生冗余的组件节点,给调试带来困难。下面用一个demo来对比一下两种实现方式。Class在类组件场景中定义了一个高层组件,负责监听窗口大小的变化,并将变化的值作为props传递给下一个组件。constuseWindowSize=Component=>{//生成一个高阶组件HOC,只包含监控窗口大小的逻辑classHOCextendsReact.PureComponent{constructor(props){super(props);this.state={size:this.getSize()};}componentDidMount(){window.addEventListener("resize",this.handleResize);}componentWillUnmount(){window.removeEventListener("resize",this.handleResize);}getSize(){返回window.innerWidth>1000?“大小”;}handleResize=()=>{constcurrentSize=this.getSize();this.setState({size:this.getSize()});}render(){//将窗口大小传递给真正的业务逻辑组件return;}}返回HOC;};接下来可以在自定义组件中调用useWindowSize等函数生成一个新的组件,并且有自己的size属性,例如:classMyComponentextendsReact.Component{render(){const{size}=this.props;如果(大小===“小”)返回<SmallComponent/>;否则返回;}}//使用useWindowSize生成高层组件,用于生成size属性传递给真正的业务组件exportdefaultuseWindowSize(MyComponent);我们看一下Hooks的实现HooksconstgetSize=()=>{returnwindow.innerWidth>1000?“大”:“小”;}constuseWindowSize=()=>{const[size,setSize]=useState(getSize());useEffect(()=>{consthandler=()=>{setSize(getSize())};window.addEventListener('resize',handler);return()=>{window.removeEventListener('resize',handler);};},[]);返回大小;};使用:constDemo=()=>{constsize=useWindowSize();if(size==="small")返回;否则返回;};从上面的例子来看,通过Hooks的方式封装了窗口大小,从而将其变成了一个可绑定的数据源,这样当窗口大小发生变化时,使用这个Hook的组件就会重新渲染。而且代码更加简洁直观,不会额外生成组件节点,也不会显得那么冗余。业务代码更加聚合。这是最常见的计时器的示例。classlettimer=nullcomponentDidMount(){timer=setInterval(()=>{//...},1000)}//...componentWillUnmount(){if(timer)clearInterval(timer)}HooksuseEffect(()=>{lettimer=setInterval(()=>{//...},1000)return()=>{if(timer)clearInterval(timer)}},[//...])Hooks的实现可以让代码更集中,逻辑更清晰。这种简单的写法我就不举例了。可以从字面上理解。使用函数组件确实可以节省很多代码,什么都看得懂。const[count,setCount]=useState(0);优点:让功能组件具有状态维护的能力,即状态在一个功能组件的多个渲染之间共享。易于维护状态。缺点:一旦组件有了自己的状态,就意味着如果重新创建组件,需要有一个恢复状态的过程,这通常会使组件变得更加复杂。使用方法:useState(initialState)的参数initialState为创建状态的初始值。它可以是任何类型,例如数字、对象、数组等。useState()的返回值是一个包含两个元素的数组。第一个数组元素用于读取状态的值,第二个用于设置状态的值。这里注意状态变量(示例中的count)是只读的,所以我们必须通过第二个数组元素setCount来设置它的值。如果我们要创建多个状态,那么我们需要多次调用useState。状态应该存储什么样的值?一般来说,我们要遵循的原则之一是:不要在状态中存储可以计算的值。从道具传递的价值。props传递的值有时不能直接使用,必须在UI上计算后再显示,比如排序。那么我们要做的就是每次使用的时候重新排序,或者使用一些缓存机制,而不是直接把结果放在state中。从URL读取的值。例如,有时需要读取URL中的参数并将其作为组件状态的一部分。那么我们每次需要使用的时候都可以从URL中读取,而不是直接读出来直接放入状态。从cookie、localStorage读取的值。一般来说也是每次需要用到的时候直接读取,而不是放在读取完之后的状态。useEffect:执行副作用useEffect(fn,deps);useEffect,顾名思义,就是用来执行副作用的。什么是副作用?一般来说,副作用是指一段与当前执行结果无关的代码。比如在函数外修改一个变量,发起一个请求等等。也就是说,在函数组件当前执行过程中,useEffect中代码的执行不会影响渲染的UI。对应Class组件,那么useEffect涵盖了ComponentDidMount、componentDidUpdate和componentWillUnmount这三个生命周期方法。但是如果你习惯使用Class组件,就不要按照useEffect映射到一个或几个生命周期的方法。你只需要记住useEffect就是判断依赖关系,在每次组件渲染的时候执行。useEffect还有两个特殊的用法:无依赖,依赖为空数组。我们来详细分析一下。如果没有依赖项,它将在每次渲染后重新执行。例如:useEffect(()=>{//console.log('rendering.......');})必须在每次渲染完成时执行;如果使用空数组作为依赖,则只会在第一次执行时触发,执行时触发,对应Class组件为componentDidMount。例如:useEffect(()=>{//在组件第一次渲染时执行,相当于类组件中的componentDidMountconsole.log('didmount.......');},[]);SummaryUsage:综上所述,useEffect允许我们在以下四次执行回调函数以产生副作用:每次渲染后执行:不提供第二个依赖参数。例如useEffect(()=>{})。仅在第一次渲染后执行:提供一个空数组作为依赖项。例如useEffect(()=>{},[])。第一次执行并在依赖项发生变化后执行:提供一个依赖项数组。例如useEffect(()=>{},[deps])。组件卸载后执行:返回回调函数。例如useEffect()=>{return()=>{}},[])。useCallback:缓存回调函数useCallback(fn,deps)为什么要用useCallback?在React函数组件中,每个UI更改都是通过重新执行整个函数来完成的,这与传统的类组件有很大不同:函数组件没有直接的方法来维护渲染之间的状态。函数Counter(){const[count,setCount]=useState(0);consthandleIncrement=()=>setCount(count+1);return+}想想这个过程。每次组件状态发生变化时,功能组件实际上都会重新执行。在每次执行时,实际上都会创建一个新的事件处理函数handleIncrement。这也就意味着,即使计数没有发生变化,但是当函数组件由于其他状态改变而重新渲染时(函数组件被重新执行),这种写法每次都会创建一个新的函数。创建一个新的事件处理程序,虽然不影响结果的正确性,但实际上是没有必要的。因为这样做不仅增加了系统的开销,更重要的是:每次都创建一个新函数的方式会使得接收事件处理器的组件需要重新渲染。例如,此示例中的按钮组件接收handleIncrement作为属性。如果每次都是新的,那么这个React就会认为这个组件的props发生了变化,所以必须重新渲染。因此,我们需要做的是:只有当count发生变化时,我们才需要重新定义一个回调函数。而这正是useCallbackHook所做的。从'react'导入React,{useState,useCallback};函数Counter(){const[count,setCount]=useState(0);consthandleIncrement=useCallback(()=>setCount(count+1),[count],//只有当计数改变时,回调函数才会被重新创建);return+}useMemo:缓存计算结果useMemo(fn,deps);useCallback(fn,deps)等同于useMemo(()=>fn,deps)。这里fn是产生所需数据的计算函数。一般来说,fn会使用deps中声明的一些变量来生成一个结果,用于渲染最终的UI。这个场景应该很容易理解:如果某个数据是从其他数据计算出来的,应该只需要在使用的数据,也就是依赖数据发生变化时才需要重新计算。避免重复计数通过useMemoHook,可以在使用的数据没有变化的情况下避免重复计数。虽然例子展示的是一个很简单的场景,但是如果是复杂的计算,对于提升性能会有很大的帮助。例如:constcalc=(a,b)=>{//假设这里做复杂的计算,暂时用幂来模拟returna**b;}constMyComponent=(props)=>{const{a,b}=道具;constc=calc(a,b);returnc:{c}
;}如果calc计算需要1000ms,那么每次渲染都要等那么久,如何优化?当a和b的值不变时,得到的c一定是一样的。所以我们可以使用useMemo来缓存这个值,避免重复计算同一个结果。constcalc=(a,b)=>{//假设这里进行了复杂的计算,暂时使用幂来模拟returna**b;}constMyComponent=(props)=>{const{a,b}=props;//缓存constc=React.useMemo(()=>calc(a,b),[a,b]);returnc:{c}
;}useCallback的功能其实是可以用useMemo实现的:constmyEventHandler=useMemo(()=>{//返回一个函数作为缓存的结果return()=>{//在这里做事件处理}},[dep1,dep2]);总结:我有这种感觉。Hook其实就是建立一种关系,将某个结果绑定到依赖的数据上。只有当依赖关系发生变化时,才需要检索此结果。useRef:在多个渲染之间共享数据constmyRefContainer=useRef(initialValue);我们可以把useRef看作是在函数组件之外创建的一个容器空间。在这个容器上,我们可以通过唯一的current属性设置一个值,从而在函数组件的多个render之间共享这个值。useRef的重要作用1.存储交叉渲染数据使用useRef保存的数据一般与UI的渲染无关,所以当ref的值改变时,不会触发组件的重新渲染,这也是useRef和使用useState的地方之间的区别。示例:const[time,setTime]=useState(0);//定义一个类似timer的容器,用于保存跨组件渲染之间的变量consttimer=useRef(null);consthandleStart=useCallback(()=>{//使用current属性设置ref的值timer.current=window.setInterval(()=>{setTime((time)=>time+1);},100);},[]);2.保存一个DOM节点的引用,在某些场景下,我们必须获取真实DOM节点的引用,所以结合React的ref属性和useRef的Hook,我们可以获取到真实DOM节点,并对其进行操作这个节点。React官方例子:functionTextInputWithFocusButton(){constinputEl=useRef(null);constonButtonClick=()=>{//current属性指向真实的输入DOM节点,所以可以调用焦点方法inputEl.current.focus();};return(<>聚焦输入>);}理解:可以看到ref的属性提供了获取一个DOM节点的能力,并使用useRef保存这个节点的应用。这样,一旦input节点渲染到界面上,我们就可以通过inputEl.currentuseContext访问到真正的DOM节点实例:为什么要用useContext来定义全局状态?在React组件之间传递状态只有一种方式,那就是通过props。缺点:这种传递关系只能在父子组件之间进行。那么问题来了:如何跨层或者同层的组件之间共享数据?这就涉及到一个新的命题:全局状态管理。react提供的解决方案:Context机制。具体原理:React提供了Context等机制,可以从某个组件开始在组件树上创建一个Context。这样,这个组件树上的所有组件都可以访问和修改这个Context。那么在函数组件中,我们可以使用像useContext这样的Hook来管理Context。使用:(这里用的是官方的例子)constthemes={light:{foreground:"#000000",background:"#eeeeee"},dark:{foreground:"#ffffff",background:"#222222"}};//创建主题上下文constThemeContext=React.createContext(themes.light);functionApp(){//整个应用使用ThemeContext.Provider作为根组件return(//使用themes.dark作为当前Context);}//在Toolbar组件中使用一个使用Theme的Button函数Toolbar(props){return(
);}//在ThemeButton中使用useContext获取当前主题函数ThemedButton(){consttheme=useContext(ThemeContext);return(我是根据主题上下文设置样式的!);}优点:上下文提供了一种在多个组件之间共享数据的便捷机制。缺点:Context相当于在React的世界里提供了一种定义全局变量的机制,而全局变量意味着两点:1.会让调试变得困难,因为你很难追踪到一个Context的变化是如何产生的。2.使组件重用变得困难,因为如果一个组件使用了某个Context,它必须保证在它的父组件使用它的路径上必须有这个Context的Provider。实际应用场景由于以上缺点,在React的开发中,除了Theme、Language等需要全局设置的变量,我们很少使用Context来共享过多的数据。需要再次强调的是,Context为React应用提供了更强大的机制,使其具备定义全局响应式数据的能力。另外,很多状态管理框架,比如Redux,都是通过Context机制来提供组件间更加可控的状态管理机制。因此,了解Context的机制也可以让我们更好的理解Redux等框架的实现原理。最后,我觉得这次的内容并不算多。其实,在学习了useState和useEffect这两个核心的Hook之后,我们就可以基本完成大部分React功能的开发了。useCallback、useMemo、useRef和useContext。这些Hooks都是为了解决功能组件中遇到的特定问题而设计的。还有几个附带的hook这里就不写了。有兴趣的可以移步官方文档看看。码字不易,但大佬们的指导和沟通也很难~TeamTNTWeb-腾讯新闻前端团队,TNTWeb致力于行业前沿技术的探索和个人能力的提升团队成员。针对前端开发者,我们整理了最新的小程序和web前端技术的优质内容,每周更新?,欢迎star,github地址:https://github.com/tnfe/TNT-每周