React笔记(三):功能(无状态)组件、渲染组件、高阶组件

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-Reduxconnect()
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)

React 高阶组件浅析 - SegmentFault 思否

高阶组件 – React (reactjs.org)

Refs 转发 – React (reactjs.org)

深入理解 React 高阶组件 - 简书 (jianshu.com)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值