从Mixin到hooks,谈谈对React16.7.0-alpha中即将引入的hooks的理解
时间:2023-04-02 17:44:52
HTML
从Mixin到hooks,再说说对hooks的理解,React16.7.0-alpha会引入代码复用,先后发布了Mixin、HOC、Renderprops等几个解决方案。另外,对于函数组件,在Reactv16.7.0-alpha中提出了ho??oks的概念,在无状态函数组件中引入了一个独立的状态空间,也就是说在函数组件中,类组件中的状态也可以是引入了组件生命周期,让功能组件更加丰富多彩。此外,钩子还保证了逻辑代码的可重用性和独立性。本文从类组件的复用方案入手,介绍了从Mixin、HOC到Renderprops的演变,最后介绍了Reactv16.7.0-alpha中的hooks以及自定义一个hooks的介绍MixinHOCRenderpropsReacthooks以及如何实现的原文地址自定义一个hooks在我的博客:https://github.com/forthealll...欢迎star和fork~1.MixinMixin是最早解决复用组件中业务逻辑代码的方案。下面介绍如何适配Mixin。下面是一个Mixin的例子:constsomeMixins={printColor(){console.log(this.state.color);}setColor(newColor){this.setState({color:newColor})}componentDidMount(){..}}下面是一个使用了Mixin的组件:classAppleextendsReact.Component{//只是为了演示,mixins一般都是通过React.createClass创建,ES6mixins中没有这种写法:[someMixins]constructor(props){super(props);this.state={color:'red'}this.printColor=this.printColor.bind(this);}render(){returnThisisanApple
}}mixin在类中引入公共业务逻辑:mixins:[someMixins]从上面例如,我们总结一下mixin的缺点:Mixin可以多个存在,以数组的形式存在,并且Mixin中的函数可以调用setState方法组件中的state,所以如果同一个state在多个Mixin模块中被修改,将无法确定状态的更新源。ES6类支持继承模式,不支持Mixins,Mixin会有覆盖。例如,如果两个Mixin模块具有相同的生命周期函数或具有相同函数名称的函数,那么将覆盖相同的函数。Mixin已被废除。具体缺陷请参考MixinsConsideredHarmful2.HOC??为了解决Mixin的缺陷,第二种解决方案是高阶组件(简称HOC)。一、HOC的几种形式举例??HOC简单理解为一个组件工厂,它接受原来的组件作为参数,添加功能和服务后返回一个新的组件。下面描述了HOC参数的几个示例。(1)参数只是原组件constredApple=withFruit(Apple);(2)参数为原组件和一个对象constredApple=withFruit(Apple,{color:'red',weight:'200g'});但是这种情况比较少见。如果对象只传递属性,其实可以通过组件的props来实现值的传递。我们HOC的主要目的是将业务、UI的展示以及一些组件的属性和状态进行分离,我们一般用props来指定比较方便(3)参数是原组件和一个函数constredApp=withFruit(App,()=>{console.log('我是水果')})(4)Currying最常见的是只使用一个原始组件作为参数,但是将业务逻辑包裹在外层,比如在react-redux的conect函数中:classAdminextendsReact.Component{}constmapStateToProps=(state)=>{返回{};}constmapDispatchToProps=(dispatch)=>{return{}}constconnect(mapStateToProps,mapDispatchToProps)(Admin)2.HOC的缺点HOC解决了Mixin的一些缺陷,但是HOC本身也有一些缺点:(1)难以追踪,而且还有一个属性覆盖的问题如果原来的组件A是通过工厂函数1,工厂函数2,工厂函数3...构建的,最后生成组件B,我们知道组件B中有很多props是与组件A不同,但我们只通过组件B,无法知道哪个组件来自哪个工厂函数。同时,如果两个工厂函数同时修改组件A的一个同名属性,就会出现属性覆盖的问题,会使之前工厂函数的修改结果失效。(2)HOC是静态构建的??所谓静态构建就是生成一个新的组件,不会马上渲染。HOC组件工厂静态构建一个组件,类似于重新声明组件的一部分。也就是说,HOC工厂函数中的生命周期函数只会在新组件渲染时执行。(3)会产生无用的空组件。3.RenderPropRenderProps顾名思义,也是一种剥离复用逻辑代码,提高组件复用性的解决方案。在复用的组件中,通过一个名为“render”的属性(属性名可以不是render,只要取值为函数即可),该属性是一个接受一个对象并返回一个子组件的函数,这个函数中的对象参数将作为道具传递给新生成的组件。这种方式与直接在父组件中将父组件中的状态传给子组件的区别在于,通过RenderProps,不需要把子组件写死,可以动态判断哪个子组件是哪个子组件父组件需要渲染。或者再总结一点:RenderProps就是一个函数,作为属性赋值给父组件,让父组件根据属性渲染子组件。(1)标准的父子组件通信方式先看类组件中常用的父子组件。父组件通过props将自己的状态传递给子组件。classSonextendsReact.Component{render(){const{feature}=this.props;return
我的头发是{feature.hair}我的鼻子是{feature.nose}
}}classFatherToSonextendsReact.Component{constructor(){this.state={hair:'black',nose:'high'}}render(){return
}}我们定义了父组件FatherToSon,它有自己的状态,并且通过props将自己的状态传递给子组件。这是一种常见的使用组件props在父子之间传递值的方式。这个值可以是一个变量,一个对象,或者一个方法,但是它只能对一个特定的子组件使用一次。如果有一个Daughter组件想要重用父组件中的方法或状态,那么必须构建一个新的组件::'high'}}render(){return}}从上面的例子可以看出,通过标准模式的父子组件通信方式可以传递state和function父组件,但无法实现复用。(2)RenderProps的介绍我们基于RenderProps的特点:RenderProps是一个函数,作为一个属性赋值给父组件,让父组件根据这个属性渲染子组件。重新实现上面(1)中的示例。类FatherChild扩展React.Component{constructor(){this.state={hair:'black',nose:'high'}}render(){{this.props.render}}}这时候如果子组件想复用父组件中的属性或者函数,可以直接使用。比如子组件Son现在可以直接调用:()}/>如果子组件Daughter想复用父组件的方法,可以直接调用:()}/>从这个例子可以看出,通过RenderProps,我们也实现了一个组件工厂,可以实现业务逻辑代码的复用。与HOC相比,RenderProps具有以下优势。不用担心道具的命名,可以追溯。子组件的props必须来自于直接父组件,是动态构建的。RenderProps也有一个缺点:无法利用SCU的生命周期来优化渲染性能。4.ReactConf2018中提出了Reacthooks的介绍以及如何自定义一个hook‖‖hooks的概念,将在以后的版本中引入。Hooks遵循函数式编程的概念,主要目的是将类组件引入到函数组件的状态和生命周期中,同时也可以将这些状态和生命周期函数抽取出来实现复用,同时降低函数组件的复杂度和易用性.hooks相关的定义还在beta阶段,可以在Reactv16.7.0-alpha中体验。为了渲染hooks定义的功能组件,React-dom的版本也必须是v16.7.0-alpha。引入hooks首先要安装:npmi-sReact@16.7.0-alphanpmi-sReact-dom@16.7.0-alphahooks主要由三部分组成,StateHooks、EffectHooks和CustomHooks,分别是下面一一介绍。(1)StateHooks与类组件相同。这里的state就是状态的意思。状态被引入功能组件。同时类组件中更新状态的方法是setState,在StateHooks中也有对应的更新状态的方法。functionExampleWithManyStates(){//声明各种状态并更新相应的状态方法const[age,setAge]=useState(42);const[fruit,setFruit]=useState('香蕉');const[todos,setTodos]=useState([{text:'LearnHooks'}]);//...}上面声明了3个Statehooks,对应的方法是useState,创建一个传入的初始值,创建一个状态。返回一个标识状态的变量,以及一个更新状态的方法。从上面的例子我们可以看出,一个功能组件可以通过useState创建多个状态。另外,StateHooks的定义必须在函数组件的最高层,不能在嵌套、循环等语句中使用。functionExampleWithManyStates(){//声明各种状态并更新相应的状态方法if(Math.random()>1){const[age,setAge]=useState(42);const[todos,setTodos]=useState([{text:'LearnHooks'}]);}else{const[fruit,setFruit]=useState('banana');const[todos,setTodos]=useState([{text:'LearnHooks'}]);}//...}?上面的方法是不允许的,因为一个函数组件可以有多个StateHooks,而useState返回的是一个数组,数组的每个元素都没有标识信息,完全取决于调用useState的顺序判断哪个状态对应哪个变量,所以必须保证useState用在函数组件的最外层。另外后面要介绍的EffectHooks的函数useEffect也必须在函数组件的最外层,后面会详细说明。(2)EffectHooks??通过StateHooks定义组件的状态,同样通过EffectHooks引入生命周期。效果挂钩通过useEffect方法以极其简化的方式引入生命周期。让我们看一个更新的例子:import{useState,useEffect}from'react';functionExample(){const[count,setCount]=useState(0);}useEffect(()=>{document.title=`您点击了${count}次`;});return(你点击了{count}次
setCount(count+1)}>点击我 );}以上是一个通过useEffect实现组件生命周期的例子。useEffect整合了componentDidMount和componentDidUpdate,也就是说useEffect的功能会在componentDidMount和componentDidUpdate期间执行一次。另外,为了实现componentWillUnmountPeriodic函数的生命周期,如果useEffect函数的返回值是一个函数,则定义这个函数为componentWillUnmount周期内执行的函数。useEffect(()=>{//componentDidMount和componentDidUpdate循环的函数体return()=>{//componentWillUnmount循环的函数体}});如果有多个useState和useEffect,则必须写成顺序定义一个useState后,立即使用一个useEffect函数。useState('Mary')useEffect(persistForm)useState('Poppins')useEffect(updateTitle)因此,和useState一样,useEffect函数也必须位于函数组件的最高层。(3)EffectHooks的补充上面我们知道,useEffect其实包括componentDidMount和componentDidUpdate。如果我们的方法只想在componentDidMount处执行,我们必须传递一个空数组作为第二个参数。useEffect(()=>{//只有在componentDidMount时才会执行},[]);上述方法只会在componentDidMount时执行,即函数组件第一次渲染时执行,执行后不会及时更新。另外,为了减少不必要的状态更新和渲染,可以这样做:useEffect(()=>{//OnlyexecutewhencomponentDidMount},[stateName]);在上面的例子中,只有stateName的值发生了变化,useEffect函数才会被执行。(4)CustomHooksCustomhooks可以将useState和useEffect的state和lifecycle函数分开,形成一个新的函数,是一个自定义封装的hooks。这是我写的一个hooks--->dom-location,可以这样引入:npmi-sdom-location,可以在函数组件中使用。这个自定义的钩子也很简单,封装了状态和生命周期函数。import{useState,useEffect}from'react'constuseDomLocation=(element)=>{let[elementlocation,setElementlocation]=useState(getlocation(element));useEffect(()=>{element.addEventListener('resize',handleResize);return()=>{element.removeEventListener('resize',handleResize);}},[]);函数handleResize(){setElementlocation(getlocation(element));}functiongetlocation(E){letrect=E.getBoundingClientRect()lettop=document.documentElement.clientTopletleft=document.documentElement.clientLeftreturn{顶部:rect.top-顶部,底部:rect.bottom-顶部,左侧:rect.left-左,右:rect.right-左};}returnelementlocation}然后直接在函数中使用:importuseDomLocationfrom'dom-location';functionApp(){....letobj=useDomLocation(element);}