React高阶组件以及使用场景

本文章重点讲述React高阶组件的使用场景,附带讲述高阶函数,因为两者很类似。对比起来更容易理解

什么是高阶函数:一个函数的参数是一个函数,或者 函数的返回值是一个函数,我们称这类函数是高阶函数。

例如:

const Mozi = function(name1){
   return function (name2){
        return name1 + ' ' + name2;
    };
};

上面函数的执行过程如下

> Mozi('墨子')  
<- function(name2){
        return name1 + ' ' + name2;
   }
> Mozi('墨子')('工程')  
<- '墨子 工程'

执行函数Mozi('墨子') 返回的是一个函数,我们执行 Mozi('墨子')('工程') 相当于执行了返回的函数

高阶函数实际的作用是什么?

以下面例子,我们要把数组的每一项变成mozi_n的方式

我们一般会用下面的方法进行实现

function format(item) {
    return  `mozi_${item}`;
}

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var result = [];
for (var i=0; i<arr.length; i++) {
    result.push(format(arr[i]));
}
console.log(result) // ["mozi_1", "mozi_2", "mozi_3", "mozi_4", "mozi_5", "mozi_6", "mozi_7", "mozi_8", "mozi_9"]

如果用高阶函数map来实现,如下

function format(item) {
    return  `mozi_${item}`;
}
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(format);  // ["mozi_1", "mozi_2", "mozi_3", "mozi_4", "mozi_5", "mozi_6", "mozi_7", "mozi_8", "mozi_9"]

上例中map把对数组的遍历进行了抽象封装,我们在对数组进行操作的时候不用关心map内部是怎么实现的,只需要将我们对数组的每一项的处理函数传进去,就会返回处理后的新数组。

这样做的好处是简洁,通用,我们再对数组进行其他操作的时候就不需要重复写for循环来处理,例如

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// 把数组每一项转换成字符串,只需要一行代码
arr.map(String);  // ["1", "2", "3", "4", "5", "6", "7", "8", "9"]

上面两个例子来简单描述了一下什么是高阶函数,以及简单用法。(有空再看一下MOZI中对redux connect的封装)

什么是React高阶组件:一个组件的参数是组件,并且返回值是一个组件,我们称这类组件为高阶组件

React 中的高阶组件主要有两种形式:属性代理反向继承
属性代理: 是 一个函数接受一个 WrappedComponent 组件作为参数传入,并返回一个继承了 React.Component 组件的类,且在该类的 render() 方法中返回被传入的 WrappedComponent 组件
属性代理形如:

function MoziComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}

属性代理类高阶组件可以做什么?

  • 操作 props
  • 用其他元素包裹传入的组件 WrappedComponent
  • 通过 ref 访问到组件实例
  • 抽离 state

一:操作 props(增加新props)

function MoziComponent(WrappedComponent) {
   return class extends WrappedComponent {
        render() {
            const newProps = {
                mozi: 'mozi'
            }
      return <WrappedComponent {...this.props,..newProps} />
        }
    };
}

二:用其他元素包裹传入的组件 WrappedComponent

function MoziComponent(WrappedComponent) {
   return class extends WrappedComponent {
        render() {
            const newProps = {
                mozi: 'mozi'
            }
      return  <div style={{display: 'block'}}>
                    <WrappedComponent/>
               </div>
        }
    };
}

三:通过 ref 访问到组件实例(在高阶组件中获取到组件实例可以直接操作dom,比如input,但是实用场景很少,有些很难用state去实现的复杂动画之类的效果,用ref直接操作dom更方便一些)

import React, { Component } from 'react';

const header_hoc = WrappedComponent => {
  return class  header_hocmozi extends Component {
   
      renderHeader = () => {
          return <div style={{margin:'0 auto',textAlign:'center'}}>
              全局头部
          </div>
      }
      getRef(ComponentRef) { // 看这里
        if(ComponentRef && ComponentRef.home) {
          console.log('wrappedComponentInstance-----',ComponentRef.home)
        }
       
      }
      render() {
        const __props ={...this.props,ref:this.getRef.bind(this) }
        return <div>
               { this.renderHeader() }
               <WrappedComponent {...__props}/>
            </div>
      }
  }

}
export default header_hoc;

四:抽离 state(因为属性代理类的高阶组件可以造作一切react有的方法或者属性,但是不建议在高阶函数里操作state,因为state属于一个组件的私有行为,一旦在高阶组件里做了一些state的更改删除之类的,容易把原有组件覆盖或者误操作,出了问题也很难去定位。)

反向继承:是 一个函数接受一个 WrappedComponent 组件作为参数传入,并返回一个继承了该传入 WrappedComponent 组件的类,且在该类的 render() 方法中返回 super.render() 方法。
是因为传入组件被动地被返回的Mozi继承,而不是 WrappedComponent 去继承 Mozi。通过这种方式他们之间的关系倒转了,所以叫反向继承。
反向继承形如:

function MoziComponent(WrappedComponent) {
   return class Mozi extends WrappedComponent {
        render() {
            return super.render();
        }
    };
}

反向继承因为继承了传入的组件,所以你可以获取传入组件(WrappedComponent)的state,props,render,以及整个生命周期

可以用反向继承高阶组件做什么?
  • 渲染劫持
  • State 抽象和更改
1 - 条件渲染【渲染劫持】

如下,通过 props.isLoading 这个条件来判断渲染哪个组件

const withLoading = (WrappedComponent) => {
    return class extends WrappedComponent {
        render() {
            if(this.props.isLoading) {
                return <Loading />;
            } else {
                return super.render();
            }
        }
    };
}
2 - 修改要渲染的元素【渲染劫持】

以下demo只是展示了如何通过修改element,意味着你可以在高阶组件中随意控制返回你要渲染的元素。
比如:我们的权限控制有页面级别的,也有细粒度的按钮级别的,渲染劫持类高阶组件就最适合细粒度的把控每个小组件的渲染权限,根据接口返回是否要展示某一个按钮 or 某一个组件。(并且不侵入具体业务代码,如果没有用高阶组件去抽离这部分细粒度的权限控制你就需要在业务代码中去写,会变得很难维护)

/*
组件修改,组件级别权限控制
*/
const edit_hoc = WrappedComponent => {
  return class extends WrappedComponent {
    render() {
      const tree = super.render();
     
      let props = {
          ...tree.props,
      };

      props.children.map(item=>{
        item && item.type == 'input' && (item.props.value = '墨子工程')
      })
      
      const newTree = React.cloneElement(tree, props, tree.props.children);
      return newTree;
    }
  };
};  

export default edit_hoc;

实用场景

1- 组件渲染性能追踪
//performance_hoc.js
const hoc = WrappedComponent => {
  return class performance_hoc extends WrappedComponent {
    constructor(props) {
        super(props);
        this.start = 0;
        this.end = 0;
    }
   componentWillMount() {
        super.componentWillMount && super.componentWillMount();
        this.start = Date.now();
    }
    componentDidMount() {
      super.componentDidMount && super.componentDidMount();
      this.end = Date.now();
      console.log(`%c ${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`,'font-size:20px;color:red;');
    }
    render() {
      return super.render()
    }
  };
};  

export default hoc;

使用方式

import React, { Component } from 'react';
import hoc from './performance_hoc';
@hoc
export default class Performance extends Component {
  render() {
    return <div>墨子工程</div>
  }
}

2 - 权限控制
import React, { Component } from 'react';

const auth_hoc = role => WrappedComponent => {
  return class extends React.Component {
      state = {
          permission: false,
          loading: true
      }

      showLoading = (isShow) => {
        this.setState({
            loading: isShow,
        });
      }

      checkAuth = () => { // 模拟返回角色的api
       return new Promise((reslove,reject)=>{
          this.showLoading(true)
          setTimeout(_=>{
            reslove('admin')
            this.showLoading(false)
          },2000)
        })
      }

      async componentWillMount() {
          const __role = await this.checkAuth();
          this.setState({
              permission: __role == role,
          });
      }

      render() {
        const { loading, permission } = this.state

          if(loading) {
            return <div>loading...</div>
          }

          if (permission) {
              return <WrappedComponent {...this.props} />;
          } else {
              return (<div>您没有权限查看该页面!</div>);
          }
      }
  };
}
export default auth_hoc;

使用(比如:产品需求是这个页面只有admin方式的才能展示 or 只有vip方式的才能展示,就可以通过高阶组件去抽离,你也不需要每一个权限都写一个高阶组件,只需要 传递不同的权限去接口验证就ok)

import React, { Component } from 'react';
import auth_hoc from './auth_hoc'; 
@auth_hoc('admin') //vip
export default class AuthPage extends Component {
  constructor(...args) {
    super(...args);
  }
  render() {
    return (
      <div className="Home">
         墨子工程
      </div>
    );
  }
}

3 - 组件&逻辑复用
//header_hoc.js
import React, { Component } from 'react';

const header_hoc = WrappedComponent => {
  return class  header_hocmozi extends Component {
   
      renderHeader = () => {
          return <div style={{margin:'0 auto',textAlign:'center'}}>
              全局头部
          </div>
      }
      render() {
        return <div>
               { this.renderHeader() }
               <WrappedComponent {...this.props}/>
            </div>
      }
  }

}
export default header_hoc;

使用

import React, { Component } from 'react';
import header_hoc from './header_hoc';

@header_hoc
export default class Home extends Component {
  render() {
    return (
      <div className="Home">
        首页
      </div>
    );
  }
}

高阶组件其实就是装饰器模式在 React 中的实现:通过给函数传入一个组件,在函数内部对该组件进行功能的增强,最后返回这个组件

总结:

  • 高阶组件 不是组件,是 一个把某个组件转换成另一个组件的 函数
  • 高阶组件的主要作用是 代码复用
  • 高阶组件是 装饰器 在 React 中的实现
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345