React 高级组件

原文转载地址:http://www.karmagut.me/?p=549

本文将探讨如何构建更易于复用,更为灵活的React高级组件。

在实际的应用开发中,多个React组件之间可能需要共用一段完成某些特定功能的代码,那么如何在不同的组件间复用这段代码就成了一个值得思考的问题(或者说,如何创建具有类似功能的,不同的React组件)。

在JavaScript的世界中,函数是第一等的公民,既能够作为参数传递,也能作为函数的返回值,所以通过高阶函数的形式,我们能够在一个函数中包装另一个传入的函数并返回一个新的函数,从而在原函数基础上实现更加丰富的功能。

而React组件不是纯函数就是ES6类(本质上也是函数)的形式,所以我们可以借鉴高阶函数的思想,来构建我们的高阶组件。

一. 高阶组件的概念及应用

高阶组件(Higher Order Component, HOC)实际上也是React组件,只不过它接收其他的React组件作为参数,并返回一个新的React组件(听起来是不是十分类似于高阶函数呢),从而增强传入组件的功能。

高阶组件的实现方式可以分为两类:
(1)代理方式的高阶组件;
(2)继承方式的高阶组件;

1.代理方式的高阶组件:

所谓的代理,就是指当前的高阶组件只是传入组件的代理,返回的新组件必然会用到传入的组件。基本上就是以下这种形式:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

import React from "react";

/* 如果包裹组件不需要除render之外的生命周期函数或者是维护自己的状态,也可以写成无状态组件的形式 */

function ProxyHigherOrderComponent(WrappedComponent){

    return class WrappingComponent extends React.Component{

        render(){

            return (

                <WrappedComponent />

            )

        }

    }

}

export default ProxyHigherOrderComponent;

//使用:

const NewComponent = ProxyHigherOrderComponent(OldComponent)

<NewComponent />

当然,我们这里的代理组件只是简单的在JSX中返回传入的组件,并没有增加任何的功能。不过代理方式的高阶组件大体上就是这种形式。

代理方式的高阶组件可应用在以下场景中:
(1)操纵props;
(2)访问ref;
(3)抽取状态;
(4)包装组件;

(1)操纵props

采用代理方式构造的高阶组件,被包裹组件接收外部数据(props)的任务就必须交由返回的包裹组件来完成,那么在这个过程中,我们就可以对props进行一定的操作,比如增减,删除或者是修改传递给被包裹组件的props列表。

例如,以下高阶组件的功能是可以删除传入的特定的prop:

1

2

3

4

5

6

function removeSpecialProp(WrappedComponent){

    return function newComponent(props){

        const { specialProp, ...otherProps } = props;

        return <WrappedComponent { ...otherProps } />

    }

}

当然,你也可以增加特定的prop,一切都根据实际需求决定:

1

2

3

4

5

function removeSpecialProp(WrappedComponent, newProps, newProp){

    return function newComponent(props){

        return <WrappedComponent { ...otherProps, newProp } { ...newProps } />

    }

}

不过这种方式也有一种缺点,那就是必须要求被包裹的组件(WrappedComponent)能够接收特定名称的prop属性,否则即使你传递一个新值进去,那也是无效的。(之后会介绍以函数为子组件的方法来避免这一缺陷)

(2)访问ref

需要明确一点,在React组件中尽量不要使用ref获得组件元素(类实例)或者是DOM元素的引用然后操作它们,因为它会破坏组件的封装性。当然在极少数情况下可能还是会用到它。

本节将介绍如何通过代理高阶组件的形式,使得传入的组件都能通过ref获取并操作DOM元素。

首先,简要介绍常规组件中是如何使用ref,然后再进一步构造我们的refsHOC高阶组件。

ref提供了一种方式,用于访问在render方法中创建的DOM节点或React元素。使用ref有三种方式(其中String类型的Ref属于旧版API,已经被废弃):

1.React.createRef()

1

2

3

4

5

6

7

8

9

class MyComponent extends React.Component {

  constructor(props) {

    super(props);

    this.myRef = React.createRef();

  }

  render() {

    return <div ref={this.myRef} />;

  }

}

然后通过this.myRef.current属性就能够获取到应用ref属性的元素的引用。current属性的类型,根据使用ref属性的节点类型,也会不一样:如果应用ref属性的元素是普通的HTML元素,那么current属性获取到的就是该底层DOM元素(比如上例中的div元素);如果是自定义类组件,ref对象将接收该组件已挂载的实例作为它的current(由于无状态组件是没有对应实例的,所以不能在函数式组件上使用ref属性)。

(题外话:如果在React组件上使用ref,React会在组件加载时将DOM元素传入current属性,在卸载时则会改回null。ref的更新会发生在componentDidMount或componentDidUpdate生命周期钩子之前。)

2.回调Ref

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

class MyComponent extends React.Component {

    constructor(props){

        super(props);

        this.input = null;

        this.typeValue = this.typeValue.bind(this);

        this.useRef(element){

            this.input = element;

        }

        this.state = {

            inputText: ""

        }

    }

    typeValue(){

        this.setState({

            inputText: this.input.value

        });

    }

    render() {

        return (

            <div>

                <input

                    type={"text"}

                    ref = {this.useRef}

                    onChange={this.typeValue}

                />;

            </div>;

        );

    }

}

上例中的this.input所指向的就是对应DOM元素的引用。

以上就是ref的基本使用方法,接下来就要创建能够使传入组件(被包裹组件)访问自身的ref引用的高阶组件,在这里,将使用回调ref的方式构建:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

const refsHOC = (WrappedComponent) => {

    return class HOCComponent extends React.Component {

        constructor(props){

            super(props);

            this.linkRef = this.linkRef.bind(this);

        }

         

        linkRef(element){

            this.root = element;

        }

         

        render(){

            const props = { ...this.props, ref: this.linkRef }

            return <WrappedComponent  { ...props } />

        }

    }

}

该组件的原理同样也是增加传递给WrappedComponent的props,只不过这个newProp是函数类型的ref属性。最后,我们在返回的新组件中,通过this.root来获取对WrappedComponent组件实例的引用。

(3)抽取状态

说到抽取状态的高阶组件用法就不得不提到react-redux提供的connect()()方法,该方法接收mapStateToProps以及mapDispatchToProps这两个函数,并返回一个高阶组件,该高阶组件接收一个傻瓜组件作为参数,最后返回一个对应的容器组件。大致就是这么一个过程。

在傻瓜组件和容器组件之间的关系中,傻瓜组件并不会管理自己的状态(所以一般是通过无状态组件来实现),而所有的状态管理都交给包裹它的容器组件来完成,这种模式就是一种“抽取状态”。

接下来将具体实现一个简易的connect()()高阶组件,以体会抽取状态这一过程:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

const doNothing = () => ({});

function connect(mapStateToProps=doNothing, mapDispatchToProps=doNothing){

     

    /* 返回的这个函数才是高阶组件 */

    retrun (DumbComponent) => {

         

        /* ContainerComponent才是将要返回的容器组件 */

        class ContainerComponent extends React.Component {

            constructor(props){

                super(props);

                 

                this.handleChange = this.handleChange.bind(this);

            }

             

            componentDidMount(){

                this.unsubscribe = this.context.store.subscribe(this.handleChange);

            }

             

            componentWillMount(){

                this.unsubscribe();

            }

             

            handleChange(){

                this.forceUpdate();

            }

             

            shouldComponentUpdate(){

                //只有当接收的参数发生变化时才重新渲染

            }

             

            render(){

                const store = this.context.store;

                const propsPassToDumb = {

                    ...this.props,

                    ...mapStateToProps(store.getState(), this.props),

                    ...mapDispatchToProps(store.dispatch, this.props)

                };

                 

                return (

                    <DumbComponent {...propsPassToDumb} />

                );

            }

        };

         

         

        /*

         *往往生成的容器组件中需要被包裹一层Provider组件,以直接向内部传递store对象,

         *而无须通过props逐层传递,所以接收数据方需要定义从context对象中接收的数据的类型contextType

         */

        ContainerComponent.contextType = {

            store: React.propTypes.object

        };

         

        return ContainerComponent;

    }

}

这里的代码参考了Redux创造者Dan Abramov的connect.js explained。当然这段代码也是很好理解的,个人认为的更加完整的connect方法的实现可以点击这里

(4)包装组件

在使用代理方式构造的高阶组件中完全可以在render函数中引入起来的元素,甚至可以组合多个其他React组件,这样就能给被包裹组件添加更为丰富的行为。例如:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

const styleHOC = (WrappedComponent, style) => {

    return class HOCComponent extends React.Component {

        render(){

            return (

                <div style={style} >

                    <WrappedComponent {...this.props} />

                </div>

            );

        }

    }

}

const style = {

    color: "red"

};

const NewComponent = styleHOC(WrappedComponent, style);

有了这个高阶组件就可以给任何的一个组件补充style样式。

2.继承方式的高阶组件

继承方式的高阶组件采用继承关系关联作为参数的组件和返回的组件,例如:

1

2

3

4

5

6

7

function inheritedHigherOrderComponent(WrappedComponent){

    return class NewComponent extends WrappedComponent{

        render(){

            return super.render();

        }

    }

}

直接通过super获取父类中render方法的引用然后再调用它。值得注意的是,在继承方式下的高阶组件中,返回的NewComponent和被包裹的WrappedComponent组件实际上是继承关系,所以二者只有一个生命周期。

继承方式的高阶组件可以应用于以下场景:
(1)操纵props;
(2)操纵生命周期函数;

(1)操纵props

继承方式的高阶组件也可以操纵props,只不过由于采用的是继承方式,操纵方式不太一样:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

function AddNewStyleProp(WrappedComponent){

    return class newComponent extends WrappedComponent{

        constructor(props){

            super(props);

        }

         

        render(){

            const elements = super.render();

            const newStyle ={

                color: (elements && elements.type === "div") ? "red" : "green"

            }

            const newProps = {...this.props, style: newStyle};

            return React.cloneElement(elements, newProps, elements.props.children);

        }

    }

}

以上代码中需要注意的点就是不要忘了render()的返回值是一个JSX结构,但是实际上在内部会调用React.createElement()把该JSX结构转换为JSON对象形式表示的虚拟DOM。所以通过elements.type能够获取到该节点对象的类型。

而React.cloneElement()方法则是用于克隆一个元素(返回一个新的React元素),传入的参数分别是要克隆的节点的对象,要传入新节点的props数据,以及它的子元素节点。(分别对应着DOM节点的三要素,元素类型,元素属性,元素子节点,详情可以点击这里

实际上,继承方式的高阶组件并不太适合去操作props,一不小心就可能会改变this.props的值。所以,此种情形最好还是使用代理形式的高阶组件。

(2)操纵生命周期函数

假如应用中有一些React组件都需要定义相同逻辑的shouldComponentUpdate()生命周期方法,我们当然可以在每个组件中都添加相同的shouldComponentUpdate()方法,但是显然代码出现了重复。

如果我们采用继承形式的高阶组件就可以让传入的组件都复用共同的一段生命周期函数代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

import React from "react";

function InheritedShouldComponentUpdateHOC(WrappedComponent){

    return class WrappingComponent extends WrappedComponent{

        constructor(props){

            super(props);

        }

         

        shouldComponentUpdate(nextProps, nextState){

            return this.props !== nextProps || this.state !== nextState ;

        }

         

    }

}

export default InheritedShouldComponentUpdateHOC;

//使用:

const NewComponent_1 = InheritedShouldComponentUpdateHOC(WrappedComponent_1);

const NewComponent_2 = InheritedShouldComponentUpdateHOC(WrappedComponent_2);

<NewComponent_1 />

<NewComponent_2 />

这样,NewComponent_1和NewComponent_2组件都具有了相同的shouldComponentUpdate()方法,尽管WrappedComponent_1和WrappedComponent_2组件的内部渲染逻辑可能并不相同。

这在我们使用其他人编写的组件时也非常的有用,如果我们想在不修改组件逻辑的基础上添加新的功能,就可以采用这种形式。

操纵生命周期函数是继承形式高阶组件的特用场景,代理方式是无法修改传入组件的生命周期函数逻辑的。

3.高阶组件命名

在使用高阶组件时,必然会导致丢失被包裹的组件元素的名称,这可能会在调试的过程中造成不便,所以我们可以通过设置组件的displayName属性来确保高阶组件的名称使我们所希望的。

例如,在使用connect方法时,希望得到的容器组件名称是特定形式的名称(如Connect_XXX),可以对connect()返回的高阶组件采用如下操作:

1

2

3

4

5

6

7

function getDisplayName(WrappedComponent){

    return WrappedComponent.displayName ||

        WrappedComponent.name ||

        "Component";

}

HOCComponent.displayName =`Connect_${getDisplayName(WrappedComponent)}`;

4.曾经的Mixin

React曾经支持一个Mixin的功能,也可以提供组件间代码复用的功能(已经被废弃,因为它只能在React.createClass方式创建的组件中使用),具体是这样使用的,了解一下即可:

1

2

3

4

5

6

7

8

9

10

11

12

13

const shouldUpdateMixin = {

    shouldComponentUpdate: function(){

        return this.props !== nextProps || this.state !== nextState ;

    }

}

const SampleComponent = React.createClass({

    mixins: [shouldUpdateMixin]

     

    render: function(){

        //......

    }

});

这样,SampleComponent组件中就具有了混入的shouldUpdateMixin对象中的方法shouldComponentUpdate。

Mixins正是由于其太过于灵活才被废弃,因为它十分难以管理,并鼓励往React组件中加入状态。

二. 以函数为子组件

采用代理形式的高阶组件固化了传递给原组件的props(即原组件必须显示声明它接收某个类型的prop属性,你才能传递给它),我们可以采用以函数为子组件的方式来克服这一局限。

实际上,以函数为组件的方式就是通过在代理组件和被包裹组件之间加了一层函数,通过该函数,我们可以传递任意的参数给它,被包裹组件则从函数中获取它想要的参数,这样传递的参数就不会再有名称的限制了。例如:

下面的WrappingComponent依旧可以视为是一个代理组件,只不过它不需要传入被包裹组件作为参数(高阶组件的形式),但是它要求其子组件必须存在,且必须是一个函数,同时函数中必须返回一个JSX类型的结构。然后WrappingComponent依旧可以通过this.props.children()的形式代理自己的子函数包裹的组件,并传递参数。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

class WrappingComponent extends React.Component {

    render(){

        const thePropsThatIWantToPass = XXX;

        return this.props.children(thePropsThatIWantToPass);

    }

}

//子元素可以是类似于下面的函数:

(props) => {

    return (

        <div>

            {props.X}

            <OtherReactComponent otherName={props.XX}/>

        </div>

    ); 

}

这种方式的好处,就是传递的参数名称没有限制,非常灵活。而他们之间的函数则成为了连接父组件和底层组件的桥梁。

猜你喜欢

转载自blog.csdn.net/qq_38719039/article/details/82844964