高阶组件

高阶组件(Higher-Order Components)

高阶组件(HOC)是 React 中用于重用组件逻辑的高级技术。 HOC 本身不是 React API 的一部分。 它们是从 React 构思本质中浮现出来的一种模式。

具体来说,高阶组件是一个函数,能够接受一个组件并返回一个新的组件。

实现高阶组件的方法

  1. 属性代理 高阶组件通过包裹 react 组件来操作 props
  2. 反向继承 高阶组件继承于被包裹的 react 组件

属性代理

我们通过一个例子来说明:

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
import React, { PureComponent } from "react";
import { Spin, message } from "antd";
import API from "./api";

const MyContainer = TargetComponnet => {
  return class extends PureComponent {
    state = {
      newProps: ""
    };

    componentDidMount() {
      this.setState({ loading: true });
      API.getConfig()
        .then(res => {
          this.setState({
            newProps: res,
            loading: false
          });
        })
        .catch(error => {
          message.error("服务异常");
          this.setState({ loading: false });
        });
    }

    render() {
      const { newProps, loading } = this.state;
      return (
        <Spin style= spinning={loading}>
          <TargetComponnet newProps={newProps} {...this.props} />
        </Spin>
      );
    }
  };
};
export default MyContainer;

其中最重要的部分是 render 方法中返回了传入的 react 组件TargetComponent,同时往TargetComponent中传入了我们自己的newProps属性,这种方法即为属性代理。 使用这个高阶组件非常容易:

1
2
3
4
5
6
7
import React, { Component } from "react";
import MyContainer from "./mycontainer";

@MyContainer
export default class MyComponent extends Component {
  //...
}

有时候,我们调用高阶组件的时候需要传入一些参数,这个实现非常简单,我们修改一下上面的方法:

1
2
3
4
5
6
7
8
9
10
  //HOC
  const MyContainer = params => TargetComponnet => {
    return ...
  }
  //使用方法
  @MyContainer(params)
  export default class MyComponent extends Component {
    //...
  }

通过 refs 获取组件实例

下方例子中,我们实现了通过 ref 获取 TargetComponnet 实例并调用实例方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//HOC
import React, { Component } from "react";

const HOC = TargetComponnet => {
  return class RefHOC extends Component {
    componentDidMount = () => {
      this.target.init();
      console.log(this.target);
    };
    render() {
      return <TargetComponnet {...this.props} ref={e => (this.target = e)} />;
    }
  };
};

export default HOC;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Component } from "react";
import HOC from "./HOC";

@HOC
export default class HOCDemo extends Component {
  state = {
    name: "HOCDemo"
  };

  init = () => {
    console.log("demo");
  };

  render() {
    return <div />;
  }
}

控制台返回:

反向继承

  • react 高阶组件有一种叫反向继承(Inheritance Inversion),简称 II。

    反向继承可以这样简单实现:

1
2
3
4
5
6
7
8
9
const HOC = TargetComponnet => {
  return class HOCII extends TargetComponnet {
    render() {
      return super.render();
    }
  };
};

export default HOC;

如你所见,返回的高阶组件类HOCII继承了 TargetComponnet。这被叫做反向继承是因为 TargetComponnet 被动地被 HOCII 继承,而不是 TargetComponnet 去继承 HOCII。通过这种方式他们之间的关系倒转了。 反向继承允许高阶组件通过 this 关键词获取 TargetComponnet,意味着它可以获取到 state,props,组件生命周期(component lifecycle)钩子,以及渲染方法(render)。
我们在HOCII上打印一下this看看

1
2
3
4
5
6
7
8
9
10
11
12
const HOC = TargetComponnet => {
  return class HOCII extends TargetComponnet {
    componentDidMount() {
      console.log(this);
    }
    render() {
      return super.render();
    }
  };
};

export default HOC;

我们可以看到在HOCII中成功获取到了HOCDemo的各种属性方法

可以用反向继承高阶组件做什么?

  1. 渲染劫持(Render Highjacking)
  2. 操作 state

渲染劫持

通过渲染劫持你可以:

  1. 根据条件不同,选择性的渲染子树(此功能属性代理也能实现,这里不做赘述)
  2. 在渲染方法中读取或更改 React Elements tree,也就是 WrappedComponent 的 children
  3. 给子树里的元素变更样式
  4. 『读取、添加、修改、删除』任何一个将被渲染的 React Element 的 props

改造一下 HOCII:

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
import React from "react";

const HOC = TargetComponnet => {
  return class HOCII extends TargetComponnet {
    componentDidMount() {
      console.log(this);
    }
    render() {
      const elementsTree = super.render();
      console.log(elementsTree);
      let newProps = {};
      if (elementsTree && elementsTree.type === "input") {
        newProps = {
          value: "may the force be with you",
          style: { marginLeft: 50, marginTop: 50 }
        };
      }
      const newElementsTree = React.cloneElement(
        elementsTree,
        { ...elementsTree.props, ...newProps },
        elementsTree.props.children
      );
      return newElementsTree;
    }
  };
};

export default HOC;
1
2
3
4
///HOCDemo
render() {
    return <input />;
}

结果:

我们可以看到,HOCDemo 中的 input 的 value 和 style 已经被我们改掉了

操作 state

高阶组件可以 『读取、修改、删除』TargetComponnet 实例的 state,如果需要也可以添加新的 state。需要记住的是,你在弄乱 TargetComponnet 的 state,可能会导致破坏一些东西。通常不建议使用高阶组件来读取或添加 state,添加 state 需要使用命名空间来防止与 TargetComponnet 的 state 冲突。