React学习:高阶组件

高阶组件的定义类比于高阶函数的定义:

  • 高阶函数的定义:接收函数作为输入,输出另一个函数的一类函数,被称作高阶函数。
  • 高阶组件的定义:接受React组件作为输入,输出一个新的React组件的组件。

高阶组件本质上也是一个函数,并不是一个组件,这一点一定不要弄错。

更通俗地描述为,高阶组件通过包裹(wrapped)被传入的React组件,经过一系列处理,最终返回一个相对增强(enhanced)的React组件,供其他组件调用。

高阶组件是React 中一个很重要且比较复杂的概念,高阶组件在很多第三方库(如Redux)中都被经常使用。在项目中用好高阶组件,可以显著提高代码质量。


一、初步应用

为什么React引入高阶组件?它长什么样?怎么用?
引入原因:为了减少代码冗余,提高代码质量。
主要功能:封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用

我们先通过一个简单的例子说明一下:
假设有组件 MyComponent,要从 LocalStorage中获取数据,然后渲染数据到界面。我们可以这样写组件代码:

//功能:组件从 LocalStorage中获取数据并展示出来
class MyComponent extends React.Component{
    componentWillMount(){
        let data = localStorage.getItem('data');
        this.setState({data});
    }
    render(){
        return <div>{this.state.data}</div>;
    }
}

代码很简单,但当有其他组件也需要从 LocalStorage中获取同样的数据展示出来时,需要在每个组件都重复 componentWillMount中的代码,这显然是很冗余的。下面让我们来看看使用高阶组件可以怎么改写这部分代码。

1、一个参数
//*********  高阶组件  *********
function withPersistentData(WrappedComponent){
    //*** 返回新组件 ***
    return class extends React.Component{
        //*** 在新组件中统一处理数据 ***
        coponentWillMount(){
            let data = localStorage.getItem('data');
            this.setState({data});
        }
        render(){
            //*** 将获得到的数据以属性方式,传递给被包裹组件
            return <WrappedComponent data={this.state.data} {...this.props}/>;

            // 通过{...this.props} 把传递给当前组件的属性 传递给被包裹的组件WrappedComponent
        }
    }
}

class MyComponent2 extends React.Component{
    render(){
        return <div>{this.props.data}</div>;
    }    
    //省略其他逻辑...
}
const MyPersistentData = withPersistentData(MyComponent2);

withPersistentData就是高阶组件,它返回一个新的组件。
它在返回的新组件的 componentWillMount中统一处理从 LocalStorage中获取数据,然后将获取到的数据以属性方式传递给被包裹组件 WrappedComponent。
这样在 WrappedComponent中,就可以通过 this.props.data来展示获取到的数据了。如 MyComponent2所示,当有其他的组件也需要这段逻辑时,继续使用 withPersistentData这个高阶组件包装这些组件就可以了。

通过这个例子,可以看出高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用。高阶组件的这种实现方式,本质上是一个装饰者设计模式。

2、多个参数

高阶组件的参数并非只能是一个组件,它还可以接收其他参数。例如,组件 MyComponent3需要从LocalStorage中获取key等于name的数据,而不是上面例子中写死的key等于data的数据,此时withPersistentData这个高阶组件就不满足我们的需求了。我们可以让它再接收一个参数,来决定从 LocalStorage中获取哪个数据:

//*********  高阶组件  *********
function withPersistentData(WrappedComponent,key){
    return class extends React.Component{
        coponentWillMount(){
            let data = localStorage.getItem(key);
            this.setState({data});
        }
        render(){
            return <WrappedComponent data={this.state.data} {...this.props}/>;
        }
    }
}

class MyComponent2 extends React.Component{
    render(){
        return <div>{this.props.data}</div>;
    }
    //省略其他逻辑...
}
class MyComponent3 extends React.Component{
    render(){
        return <div>{this.props.data}</div>;
    }
    //省略其他逻辑...
}
const MyPersistentData2 = withPersistentData(MyComponent2,'data');
const MyPersistentData3 = withPersistentData(MyComponent3,'name');

高阶组件中的参数当然也可以是函数


二、进阶用法

高阶组件最常见的函数签名形式是这样的:HOC([param])([WrappedComponent])

用这种形式改写 withPersistentData,如下:

//*********  高阶组件  *********
function withPersistentData = (key) => (WrappedComponent) =>{
    return class extends React.Component{
        coponentWillMount(){
            let data = localStorage.getItem(key);
            this.setState({data});
        }
        render(){
            return <WrappedComponent data={this.state.data} {...this.props}/>;
        }
    }
}

class MyComponent2 extends React.Component{
    render(){
        return <div>{this.props.data}</div>;
    }
    //省略其他逻辑...
}
class MyComponent3 extends React.Component{
    render(){
        return <div>{this.props.data}</div>;
    }
    //省略其他逻辑...
}
const MyPersistentData2 = withPersistentData('data')(MyComponent2);
const MyPersistentData3 = withPersistentData('name')(MyComponent3);

此时的 withPersistentData已经是一个返回高阶组件的高阶函数
HOC([param])([WrappedComponent])这种形式中, HOC([param])才是真正的高阶组件,我们可以把它看成高阶组件的变种形式。这种形式的高阶组件因其特有的便利性——结构清晰(普通参数和被包裹组件分离)、易于组合,大量出现在第三方库中。如react-redux中的connect就是一个典型。connect的定义如下:

// 这个函数会将一个React组件连接到Redux 的 store
connect([mapStateToProps],[mapDispatchToProps],[mergeProps],[options])(WrappedComponent)

这个函数会将一个React组件连接到Redux 的 store。在连接的过程中,connect通过参数 mapStateToProps,从全局store中取出当前组件需要的state,并把state转化成当前组件的props;同时通过参数 mapDispatchToProps,把当前组件用到的Redux的action creators,以props的方式传递给当前组件。


例如,我们把组件ComponentA连接到Redux上的写法类似于:

const ConnectedComponentA = connect(mapStateToProps,mapDispatchToProps)(ComponentA);

我们可以把它拆分来看:

// connect 是一个函数,返回值enhance也是一个函数
const enhance = connect(mapStateToProps, mapDispatchToProps);
// 返回的函数就是一个高阶组件,该高阶组件返回一个与Redux store关联起来的新组件
const ConnectedComponentA = enhance(ComponentA);

当多个函数的输出和它的输入类型相同时,这些函数是很容易组合到一起使用的。例如,有f,g,h三个高阶组件,都只接受一个组件作为参数,于是我们可以很方便的嵌套使用它们: f(g(h(WrappedComponent)))。这里可以有一个例外,即最内层的高阶组件h可以有多个参数,但其他高阶组件必须只能接收一个参数,只有这样才能保证内层的函数返回值和外层的函数参数数量一致(都只有1个)。

例如我们将connect和另一个打印日志的高阶组件 withLog联合使用:

const ConnectedComponentA = connect(mapStateToProps)(withLog(ComponentA));

你可以使用一个组合工具:
调用compose(f,g,h)等价于(...args)=>f(g(h(...args)))

所以我们还能将上面代码改写成:

const enhance = compose(connect(mapStateToProps),withLog);
const ConnectedComponentA = enhance(ComponentA);

像Redux等很多第三方库都提供了 compose的实现, compose结合高阶组件使用,可以显著提高代码的可读性和逻辑的清晰度。


三、与父组件区别

有人可能会觉得高阶组件有些类似父组件的使用。例如,我们完全可以把高阶组件中的逻辑放到一个父组件中去执行,执行完成的结果再传递给子组件。从逻辑的执行流程上来看,高阶组件确实和父组件比较相像,但是高阶组件强调的是逻辑的抽象。高阶组件是一个函数,函数关注的是逻辑;父组件是一个组件,组件主要关注的是UI/DOM。如果逻辑是与DOM直接相关的,那么这部分逻辑适合放到父组件中实现;如果逻辑是与DOM不直接相关的,那么这部分逻辑适合使用高阶组件抽象,如数据校验、请求发送等。


四、注意事项

1)不要在组件的render方法中使用高阶组件,也不要在组件的其他生命周期方法中使用高阶组件。因为高阶组件每次都会返回一个新的组件,在render中使用会导致每次渲染出来的组件都不相等( ===),于是每次render,组件都会卸载(unmount),然后重新挂载(mount),既影响了效率,又丢失了组件及其子组件的状态。高阶组件最适合使用的地方是在组件定义的外部,这样就不会受到组件生命周期的影响了。

2)如果需要使用被包装组件的静态方法,那么必须手动拷贝这些静态方法。因为高阶组件返回的新组件,是不包含被包装组件的静态方法。hoist-non-react-statics可以帮助我们方便的拷贝组件所有的自定义静态方法。有兴趣的同学可以自行了解。

3)Refs不会被传递给被包装组件。尽管在定义高阶组件时,我们会把所有的属性都传递给被包装组件,但是 ref并不会传递给被包装组件。如果你在高阶组件的返回组件中定义了 ref,那么它指向的是这个返回的新组件,而不是内部被包装的组件。如果你希望获取被包装组件的引用,你可以把 ref的回调函数定义成一个普通属性(给它一个ref以外的名字)。下面的例子就用inputRef这个属性名代替了常规的ref命名:

function Field({ inputRef, ...rest }) {
  return <input ref={inputRef} {...rest} />;
}

// 在高阶组件中增强Field组件
const EnhancedField = enhance(Field);

// 组件的render函数中……
<EnhancedField
  inputRef={(inputEl) => {
    // 该回调函数被作为常规的props属性传递
    this.inputEl = inputEl
  }}
/>

// 现在你就可以愉快的调用控制函数了
this.inputEl.focus();

参考:深入浅出React高阶组件

猜你喜欢

转载自blog.csdn.net/b954960630/article/details/80180904
今日推荐