React笔记(三)
1.无状态组件(功能组件)
-
React是通过更新状态,来实现页面的局部动态变化。那么对于一些只做展示的组件,或者一些原子组件,比如说列表中的某一行,一个输入框等,就可以使用无状态组件。由于无状态组件内部不存在状态,因此也可以使用函数式组件进行书写。甚至如果功能比较简单,可以直接使用箭头函数书写。一些UI组件库使用的大多数都是无状态组件。
//无状态组件 const Child = (props)=>( <input type="text" value={props.value} onChange={props.fun}/> ) export default class Root extends Component { constructor(props){ super(props); this.state = { value:'' } } inputOnchange = (e)=>{ this.setState({value:e.target.value}); } render() { var {value} = this.state; return ( <div> <Child value={value} fun={this.inputOnchange}/> <p>{value}</p> </div> ) }
这段代码完成了一个受控组件的建立,并实现了数据的双向绑定。
2.容器组件和渲染组件
- 容器组件与渲染组件互补,为了符合单一职责原则,可以让一些功能,例如异步获取数据与处理数据等放置在容器组件上,在渲染组件上只进行页面的渲染。在渲染组件的功能上,其是与无状态组件之间有一定的交叉关系。在使用一些UI组件库进行页面渲染时,其达到的效果是渲染组件,但本质上还是无状态组件,也就是说其组件类型为无状态,功能为渲染。在一些比较常用的框架里,也采用了这种组件方式,例如
React-Redux
的connect()
。
3.高阶组件
-
首先了解高阶函数,高阶函数是一个返回值为函数的函数。那么类比到高阶组件,也就是返回一个组件的组件。当要保持组件功能单一性,减小耦合度时,可以尝试使用此组件。当基础业务开发完成后,需要在原有组件上追加新的业务功能,并且与组件内部的页面渲染无关时,这时也可以尝试此组件。比如说路由的校验,又或者表单的校验。
function RouteFilter(props) { //获取url路径与路由表 const {routerConfig,location} = props; const {pathname} = location; //获取当前目标路由 const targetRouterConfig = routerConfig.find( (item:any)=>{ return item.path.replace(/\s*/g,"") === pathname } ); //获取登录信息 const isLogin = sessionStorage.getItem("xxxx"); //判断目标路由是否存在,是否不需要登录权限校验,不需要的话可以直接访问 //判断用户是否登录,路由是否合规 //返回路由重定向 return <Redirect to='/....'/>; }
上段代码就是一个简单的高阶组件,实现了一个路由守卫的作用,只有路由符合规范并且登录信息合规时才会允许用户访问页面。
-
高阶组件存在属性代理(props proxy) 的实现方式,即,通过将属性赋予给外部的包装组件,在外部包装组件中对
props
进行处理,并提供给内部组件。这种方式可以广泛应用于修改props。 -
也可以理解为是一个函数,返回值是一个容器组件,在容器组件内其
render
方法返回的是渲染组件。当然,作为一个使用了属性代理的组件,其功能不仅于此。//内部渲染组件(展示父组件传递进来的内容) function Child(props) { return <p>{props.value}</p> } //高阶组件,令其返回一个包装组件,来对内部组件添加标题 function withChild(Wrap) { return class P extends Component { newProps={ value='内容替换' } render() { //this指向问题,这里的this指向的是P的实例,即外部函数的返回值,是下方的HOC //如果使用this.newProps就会替换原本传递给内部组件的props。 //当需要抽象state时,就不向内部组件传递props,全部使用包装组件的state向props传值即可 console.log(this.props); return (<div> <h4>{'标题'}</h4> <Wrap {...this.props}{...this.newProps} /> </div>); } } } //把包装组件从函数中获取出来,并把内部组件注入进去 const HOC = withChild(Child); //展示 export default class Root extends Component { render() { return ( <div> <HOC value='内容' /> </div> ) } }
-
此外还可以通过反向继承的方式实现高阶组件,即返回的高阶组件是 继承于 传给高阶组件外部函数的组件。在反向继承中,高阶组件可以使用传入组件的状态,属性,生命周期函数以及渲染方法。使用反向继承可以进行渲染劫持,或操纵传入组件的状态(state)。
class Child extends Component{ render(){ console.log('child render'); return <p>{this.props.value}</p> } } function withChild(Wrap) { return class extends Wrap { render() { if(this.props.value == '内容'){ return super.render(); } else{ return <div>错误</div>; } } } } const HOC = withChild(Child); export default class Root extends Component { render() { return ( <div> <HOC value='内容' /> </div> ) } }
上段代码就是一段渲染劫持的示例。在此段代码中,相当于高阶组件内设置了一个门卫,如果传入的props的内容不对,就不会渲染传入的组件,而是会渲染一些新的内容。另外一个功能则是修改传入组件的状态
class Child extends Component{ //传入组件定义的状态 state = { value:'内容' } //经过高阶组件修改后的状态 render(){ console.log('child',this.state.value); return <p>{this.state.value}</p> } } function withChild(Wrap) { return class extends Wrap { //高阶组件修改内部状态 componentWillMount(){ this.setState({value:'修改了'}); console.log('enhancer',this.state.value); } //渲染 render() { return super.render(); } } } const HOC = withChild(Child); export default class Root extends Component { render() { return ( <div> <HOC/> </div> ) } }
在上段代码中的控制台内可以看见打印了两条信息,第一条是‘exhancer 内容’,代表着高阶组件继承于传入组件的状态value为内容,第二条为‘child 修改了’,代表着高阶组件已经修改完毕,并进行页面渲染,渲染时高阶组件已经修改了传入组件的状态value为修改了,完成了状态修改。
-
在使用高阶组件时也会遇见问题,即静态方法丢失,由于传入的组件被高阶组件包裹起来,在外部使用高阶组件时,是访问不到传入组件的,也就访问不到传入组件的静态方法。这时,如果需要使用传入组件的静态方法的话,需要将传入组件的静态方法拷贝给高阶组件,作为高阶组件的静态方法。或者使用第三方组件库hoist-non-react-statics进行处理,用法为
hoistNonReactStatic(高阶组件,传入组件);
这个方法会将所有的非React的静态方法拷贝进高阶组件。 -
另外在使用高阶组件时,不要在render方法中使用高阶组件。这样会导致每次渲染页面(使用render)时,都会重新挂载此高阶组件。对于性能而言,其每次render时都会重新挂载与加载新的虚拟DOM元素,影响性能。但更严重的是,由于每次渲染都会重新挂载组件,会导致该组件以及其所有子组件的状态丢失。
//正确用法 const HOC = withChild(Child); export default class Root extends Component { render() { //错误用法 const HOC = withChild(Child); return ( <div> <HOC/> </div> ) } }
-
另外由于ref并不像props一样可以传递给包装组件,所以,如下所示,将ref添加到容器组件时,ref指向的并不是内部的传入组件,而是指向外部的高阶组件。
class Child extends Component{ state = { value:'内容' } refTextValue = 'child'; render(){ return <p>{this.state.value}</p> } } function withChild(Wrap) { return class extends Wrap { refTextValue = 'Enhance' render() { return super.render(); } } } const HOC = withChild(Child); export default class Root extends Component { constructor(props){ super(props); this.hoc = React.createRef(); } render() { return ( <div> <HOC ref={this.hoc}/> <button onClick={()=>{alert(this.hoc.current.refTextValue)}}>点我看Ref</button> </div> ) } }
但如果需要使用ref指定内部传入组件时,就可以使用React16.3引入的
React.forwardRef
API。其接受一个以props和ref为参数的渲染函数。下方为使用示例。class Child extends Component{ state = { value:'内容' } refTextValue = 'child'; render(){ return <p>{this.state.value}</p> } } function withChild(Wrap) { class Enhancer extends Wrap { refTextValue = 'Enhance' render() { const {forwardRef,...rest} = this.props; return <Wrap ref={forwardRef} {...rest}/> } } return React.forwardRef((props,ref)=>{ return <Enhancer {...props} forwardRef={ref}/> }) } const HOC = withChild(Child); export default class Root extends Component { constructor(props){ super(props); this.hoc = React.createRef(); } render() { return ( <div> <HOC ref={this.hoc}/> <button onClick={()=>{alert(this.hoc.current.refTextValue)}}>点我看Ref</button> </div> ) } }
只需要更改高阶组件内部的代码即可实现。其本质上是将ref以props的形式转发给高阶组件内部的传入组件,内部传入组件拿到ref后将其挂载在自己本身上。
参考文章
React中的五种组件形式 - 简书 (jianshu.com)