React之高阶组件
前言
高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件
从高阶组件的定义来看,高阶组件并不是一个组件,它就是一个函数,接受一个组件并且返回一个被包装过的新组件。
const NewComponent = higherOrderComponent(OldComponent)
React的高阶组件在React的第三方库中的应用是非常广泛的,理解好React的高阶组件,有利于我们今后更好的学习React第三方库的源码,可以让我们写出复用性更强的代码。
基本用法
React高阶组件的常见形式一般有两种,分别是属性代理(Props Proxy)的形式和反向继承(Inheritance Inversion),简称II。下面分别介绍两种形式的用法。
属性代理模式(Props Proxy)
代理模式中你可以读取、添加、修改、删除将要传递给 WrappedComponent 的 props。
假设我们有一个组件,用来从localStorage中获取数据,我们可以这么做:
import React, {
Component } from 'react'; class GetName extends Component {
constructor(props) {
super(props); this.state = {
name: '', }; } componentWillMount() {
const name = localStorage.getItem('name'); this.setState({
name }); } render() {
return ( <p>{
this.state.name }</p> ) } }
这是常规的写法,但是如果此时也有其他组件需要从localStorage中获取数据,那么组件中从localStorage中取值的部分代码就会重复。那么有什么比较好的方法可以复用这部分代码呢?当然有,下面用React属性代理模式来重写上面的代码。
import React, {
Component } from 'react'; function componentWrapper(WrappedComponent) {
class HOC extends Component {
constructor(props) {
super(props); this.state = {
name: '' } } componentWillMount() {
const name = localStorage.getItem('name'); this.setState({
name }); } render () {
const newProps = {
name: this.state.name } return ( <WrappedComponent {
...this.props} {
...newProps} /> ) } } return NewComponent; } class ShowName extends Component {
render() {
return ( <h1> {
this.props.name }</h1> ) } } const ShowNameWidthData = componentWrapper(ShowName);
这里我先定义一个函数componentWrapper,函数的参数里面传入需要包装的组件WrappedComponent,再在函数里面定义一个新的组件HOC并返回出来,在这个新的组件里面可以添加获取localStorage数据的逻辑,最后将获取到的数据通过属性传递的方式传给被包装的组件WrappedComponent,这样被返回的组件中就可以通过this.props.name的方式获取到localStorage中存储的数据了。
这里存在一个问题,代码中从localStorage中获取的key值是固定的,是name,如果我想获取其他key的值呢?我们可以通过给高阶组件传入参数的方式来指定获取的key值,代码可以这么写:
import React, {
Component } from 'react'; function componentWrapper(WrappedComponent, key) {
class HOC extends Component {
constructor(props) {
super(props); this.state = {
data: '' } } componentWillMount() {
const data = localStorage.getItem(key); this.setState({
data }); } render () {
const newProps = {
data: this.state.data } return ( <WrappedComponent{
...this.props} {
...newProps} /> ) } } return NewComponent; } class ShowName extends Component {
render() {
return ( <h1> {
this.props.data }</h1> ) } } const ShowNameWidthData = componentWrapper(ShowName, 'age');
可以看到,我们给高阶组件中传入了第二个参数来指定key值,很好的解决了上述问题,这么写没问题,但是我们我们经常在React的第三方库中看到如下用法:
HOC([param])([WrappedComponent])
这其实是函数柯里化的写法func(a, b) => func(a)(b),下面我用函数柯里化的形式来改写上面的代码:
import React, {
Component } from 'react'; function componentWrapper (key) {
return function(WrappedComponent) {
return class HOC extends Component {
constructor(props) {
super(props); this.state = {
data: '' } } componentWillMount() {
const data = localStorage.getItem(key); this.setState({
data }); } render () {
return ( <WrappedComponentdata={
this.state.data} /> ) } } } } class ShowName extends Component {
render() {
return ( <h1>{
this.props.data }</h1> ) } } const ShowNameWidthData = componentWrapper('age')(ShowName); export default ShowNameWidthData;
如果熟悉es6箭头函数(不熟悉的可以戳这里~~)的写法来改造componentWrapper会更加简洁:
function componentWrapper = (key) => (WrappedComponent)=> {
return class HOC extends Component {
// ...省略 } }
我们在使用react-redux的时候一般会这么使用:
connect(mapStateToProps, mapDispatchToProps)(TodoApp)
看着是不是很熟悉?没错,这就是高阶函数的典型应用之一。connect函数的作用连接React组件和redux store,通过mapStateToProps允许我们将 store 中的数据作为 props 绑定到TodoApp组件上,mapDispatchToProps将 action 作为 props 绑定到组件上,也会成为 TodoApp组件的 props。
如果我们在浏览器中用React Developer Tools查看React的组件树可以看到showName组件被HOC包裹起来了。

但是随之带来的问题是,如果这个高阶组件被使用了多次,那么在调试的时候,将会看到一大堆HOC,针对这个问题我们可以进行优化,能够同时显示被高阶组件包裹的组件名称。这里可以通过新增一个getDisplayName函数以及静态属性displayName,修改后的代码如下:
import React, {
Component } from 'react'; const getDisplayName = component => component.displayName || component.name || 'Component'; function componentWrapper (key) {
return function(WrappedComponent) {
return class HOC extends Component {
// 定义一个静态方法displayName来显示原有组件的名称 static displayName = `HOC(${
getDisplayName(WrappedComponent)})`; constructor(props) {
super(props); this.state = {
data: '' } } componentWillMount() {
const data = localStorage.getItem(key); this.setState({
data }); } render () {
return ( <WrappedComponent data={
this.state.data} /> ) } } } } class ShowName extends Component {
render() {
return ( <h1>{
this.props.data }</h1> ) } } const ShowNameWidthData = componentWrapper('age')(ShowName);
通过ref访问组件实例
一般如果我们想在React的父组件中使用的时候,可以通过
this.refs.myComponent的形式获取到组件真正实例的引用。但是如果一个组件经过高阶组件的包装就无法获得WrappedComponent的ref了,因为我们这时候拿到的是HOC里面返回的component。
下面提供一个解决方法:
要想通过 ref获取WrappedComponent 的实例,必须在React高阶组件的render方法中返回WrappedComponent。
在父组件中想要获取到WrappedComponent 的实例,先定义一个getInstance方法,并将这个方法作为属性传入高阶组件。高阶组件中当执行render方法的时候会先判断传入的getInstance 这个属性是否是函数,如果是就返回WrappedComponent对象的引用。
import React, {
Component } from 'react'; function HOCWrapper(WrappedComponent) {
return class HOC extends Component {
render() {
let props = {
...this.props }; if (typeof this.props.getInstance === "function") {
props.ref = this.props.getInstance; } return <WrappedComponent {
...props} /> } } } class MyComponent extends Component {
render() {
return ( <div name="bob">this is a demo</div> ) } } const Demo = HOCWrapper(MyComponent); class ParentComponent extends React.Component {
getInstance = (ref)=>{
this.wrappedInstance = ref; } componentDidMount() {
console.log(this.wrappedInstance) } render(){
return <Demo getInstance={
this.getInstance} /> } }
反向继承(Inheritance Inversion)
反向继承顾名思义就是返回的高阶组件继承了WrappedComponent。反向继承允许高阶组件通过 this关键字获取 WrappedComponent,意味着它可以获取到 state,props,组件生命周期(component lifecycle)钩子,以及渲染方法(render)。
反向继承的应用之一是”渲染劫持”,所谓”渲染劫持”就是高阶组件中由于继承了WrappedComponent,因此就控制了它的render方法,利用这点可以做各种操作。看一个经典的例子。
import React, {
Component } from 'react'; function iiHOC(config) {
return function(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
const elementTree = super.render(); const {
type, style = {
}} = config; if(type === 'app-style') {
return ( <div style={
{
...style }}> {
elementTree } </div> ) } return elementTree; } } } } class MyComponent extends Component {
render() {
return ( <div>我来啦 ~~ </div> ) } } export default iiHOC({
type: 'app-style', style: {
color: 'red', 'font-size': '20px'}})(MyComponent);
这个例子通过传入的config参数中的type值来判断是否加入样式渲染。注意这个这个高阶函数的写法,用到了函数的柯里化,主要是为了便于传参。
总结
关于React的高阶函数的总结就到这里,React的高阶函数其实本质上和其他语言中的装饰器的概念是一致的,只是一种设计模式的相互借鉴,掌握好React高阶组件的用法可以帮助我们很好的提高代码的封装性,由于很多React的第三方库中也大量使用了高阶组件的设计方法,对于我们以后阅读源码也更加游刃有余。
更多精彩内容欢迎关注我的公众号!

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/218449.html原文链接:https://javaforall.net
