mengtuifrontend / Blog

芦叶满汀洲,寒沙带浅流。二十年重过南楼。柳下系船犹未稳,能几日,又中秋。 黄鹤断矶头,故人今在否?旧江山浑是新愁。欲买桂花同载酒,终不似,少年游。
18 stars 5 forks source link

react 高阶组件 #30

Open diandiandida opened 4 years ago

diandiandida commented 4 years ago

什么是高阶组件 高阶组件是react中的一个概念,实质来讲,高阶组件就是一个高阶函数

什么是高阶函数,就是满足以下任意一个条件:

日常工作中,我们在不知不觉中就已经使用了高阶函数

const arr = [1,2,3,4,5,6];
const square = d => d ** 2;
arr.map(square) // [1, 4, 9, 16, 25, 36];

在这个例子中,我们的map函数通过传入的方法square实现了数组的二次方。 另一个我们经常会用到的例子:

<ul>
    {list ? list.map((item, index) => {
        return <li>{item.name}</li>
    })}
</ul>

由这些高阶函数我们可以明白,其实高阶组件是一种用于复用组件逻辑的高级技术,它并不是 React API的一部分,而是从React 演化而来的一种模式。具体地说,高阶组件就是:接收一个组件并返回另外一个新组件的函数,并且会继承传入组件(state,props,生命周期,render)。

用来做什么 简单来讲,提取重复代码,处理相同逻辑 高阶组件还有更多的功能:

// 操纵组件的方法
function HOC(WrappedComponent) {
  return class ExampleEnhance extends WrappedComponent {
    ...
    componentDidMount() {
      super.componentDidMount();
    }
    componentWillUnmount() {
      super.componentWillUnmount();
    }
    render() {
      ...
      return super.render();
    }
  }
}

这里我们可以看到,我们的高阶组件继承了传入组件(一般的高阶组件是继承react的component),并且在render函数中return的是super.render(),从而实现了继承反转。因此高阶组件就可以获取到传入组件的生命周期钩子和方法。

这里举一个简单的例子: 我们先创建一个被包裹的组件:

import React, { Component } from "react";

export default class ComponentChild extends Component {
  constructor(props) {
    super(props);
    this.state = {
      message: "我是ComponentChild中的message"
    };
  }
  componentDidMount() {
    console.log("ComponentChild Did Mount");
  }
  clickComponent() {
    console.log("ComponentChild click");
  }
  render() {
    return <div>{this.state.message}</div>;
  }
}

这里我们会返回一个信息,信息存在组件的state中,并且有一个clickComponent方法和一个生命周期方法,会在控制台打印信息。

接下来实现具有继承反转能力的高阶组件:

import React from "react";

//这样的方式,外部组件的 state 可以将被继承组件的 state 和 钩子函数彻底覆盖掉。同时,外部组件也可以调用被继承组件的方法。
const messageOtherHoc = WrappedComponent => {
  return class extends WrappedComponent {
    constructor(props) {
      super(props);
      this.state = {
        message: "我是messageOtherHoc中的message"
      };
    }
    componentDidMount() {
      console.log("messageOtherHoc componentDidMount");
      this.clickComponent();
    }

    render() {
      return (
        <div>
          <div onClick={this.clickComponent}>messageOtherHoc 点击我</div>
          <div>
            <div>{super.render()}</div>
          </div>
        </div>
      );
    }
  };
};
export default messageOtherHoc;

高阶组件中,我们会定义state,生命周期函数,并且在div中设置点击事件,调用this.clickComponent方法,显然,我们高阶组件中并没有写这样的方法;最后我们会返回传入组件的render函数,通过super找到传入组件

然后我们去调用这个高阶组件:

const Message2 = MessageOtherHoc(ComponentChild);
<Message2 />

打印台信息: 打印信息1

显而易见,高阶组件覆盖了传入组件的生命周期方法并且调用了传入组件的clickComponent方法。

这就意味着这样的高阶组件可以访问到WrappedComponent的state,props,生命周期和render方法。如果在高阶组件中定义了与WrappedComponent同名方法,将会发生覆盖,如果需要调用他们,我们就必须手动通过super进行调用。就像例子中那样。

如何创建一个高阶组件

一个简单易懂的例子,命名和代码可能不是规范的写法,但是可以说明问题 用于返回Tips相同的展示:

import React, { Component } from "react";

function withToolTipsSubscription(WrappedComponent) {
  return class withToolTipsSubscription extends Component {
    render() {
      const { tipsType, ...otherProps } = this.props;
      return (
        <fieldset>
          <legend>我是一个 {tipsType.type} 组件</legend>
          <p>
            <span>提示时间:{tipsType.date}</span>
          </p>
          <p>
            <span>提示:</span>
          </p>
          <span>WrappedComponent is:</span>
          <WrappedComponent {...otherProps} />
        </fieldset>
      );
    }
  };
}

export default withToolTipsSubscription;

我们的Tip可以拥有一个共同的标题,提示时间和一些文字

在这里面我们用到了操纵props,并把props继续传递给了我们的参数组件:

const { tipsType, ...otherProps } = this.props;
<WrappedComponent {...otherProps} />

AskToolTip.js

用于展示一个一个带有两个按钮的Tips

两个按钮可以和父组件进行交互,直接调用 this.props 调用父组件方法即可,无需其他操纵

import React, { Component } from "react";
import ToolTips from "./ToolTips";

class AskToolTips extends Component {
  constructor(props) {
    super(props);
    this.handleConfirm = this.handleConfirm.bind(this);
    this.handleCancel = this.handleCancel.bind(this);
    this.confirmIndex = 0;
    this.cancelIndex = 0;
  }

  handleConfirm() {
    this.props.onConfirm((this.confirmIndex += 1));
  }

  handleCancel() {
    this.props.onCancel((this.cancelIndex += 1));
  }

  render() {
    return (
      <div
        style={{
          borderWidth: "1px",
          borderColor: "#d3d3d3",
          borderStyle: "solid"
        }}
      >
        <div>
          <span>There are somthing will ask with you:</span>
          <p>
            <span>[ ConfirmTimes:{this.props.confirmTimes} ]</span>
            <span>[ ConfirmTimes:{this.props.cancelTimes} ]</span>
          </p>
        </div>
        <button onClick={this.handleConfirm}>confirm</button>
        <button onClick={this.handleCancel}>cancel</button>
      </div>
    );
  }
}

export default ToolTips(AskToolTips);

这个组件的写法和平常的组件大致无二,唯一的的区别在于 export 的内容

export default ToolTips(AskToolTips);

引入并返回我们包裹的高阶组件

ErrorToolTips.js

和上面的组件类似,但是这里只接收父组件传递的内容进行展示:

import React, { Component } from "react";
import ToolTips from "./ToolTips";

class ErrorToolTips extends Component {
  render() {
    return (
      <div
        style={{
          borderWidth: "1px",
          borderColor: "#d3d3d3",
          borderStyle: "solid"
        }}
      >
        <span>{this.props.msg.str}</span>
      </div>
    );
  }
}

export default ToolTips(ErrorToolTips);

HocExample.js

组件的具体调用:

import React, { Component } from "react";
import AskToolTips from "./HocEx/AskToolTips";
import ErrorToolTips from "./HocEx/ErrorToolTips";

export default class HocExample extends Component {
  constructor(props) {
    super(props);
    this.handlerAskConfirm = this.handlerAskConfirm.bind(this);
    this.handlerAskCancel = this.handlerAskCancel.bind(this);

    this.state = {
      confirmTimes: 0,
      cancelTimes: 0,
      date: new Date().toLocaleString(),
      msg: "我是一个错误提示框"
    };
  }

  handlerAskConfirm(index) {
    console.log("handlerAskConfirm:" + index);
    this.setState((state, props) => {
      const msg =
        "睡在我下铺的兄弟Ask我:" +
        (state.cancelTimes + index) +
        "次!,Confirm:" +
        index +
        "次!Cancel:" +
        state.cancelTimes +
        "次!";
      return {
        confirmTimes: index,
        msg: msg,
        date: new Date().toLocaleString()
      };
    });
  }

  handlerAskCancel(index) {
    console.log("handlerAskCancel:" + index);
    this.setState((state, props) => {
      const msg =
        "睡在我下铺的兄弟Ask我:" +
        (state.confirmTimes + index) +
        "次!,Confirm:" +
        state.confirmTimes +
        "次!Cancel:" +
        index +
        "次!";
      return {
        cancelTimes: index,
        msg: msg,
        date: new Date().toLocaleString()
      };
    });
  }

  render() {
    return (
      <fieldset>
        <legend>高阶组件实例</legend>
        <ErrorToolTips
          tipsType={{
            type: "ErrorToolTips",
            date: this.state.date
          }}
          msg={{ str: this.state.msg }}
        />
        <AskToolTips
          tipsType={{
            type: "AskToolTips",
            date: this.state.date
          }}
          confirmTimes={this.state.confirmTimes}
          cancelTimes={this.state.cancelTimes}
          onConfirm={e => this.handlerAskConfirm(e)}
          onCancel={e => this.handlerAskCancel(e)}
        />
      </fieldset>
    );
  }
}

我们直接调用AskToolTips和ErrorToolTips,并且传递props的方式和普通的组件没有上面不同。

效果: 效果 1 效果 2

高阶函数的具体应用 这里我就结合我们的 ReactDOM.createPortal 具体说一下:

Portal是一种将子节点渲染到存在于父组件以外的 DOM 节点的方案,相信大家都有所了解,这里也不做赘述,主要是通过 ReactDOM.createPortal 方法,第一个参数是任何可以渲染的react元素或者组件。第二个参数是作为渲染第一个参数的容器DOM 元素,最终生成的节点将会挂载到此dom元素节点上,是为了解决dom层级的问题

import React, { Component } from "react";
import ReactDOM from "react-dom";

const withPortal = WrappedComponent => {
  class AddPortal extends Component {
    constructor(props) {
      super(props);
      this.el = this.getDiv();
    }

    componentWillUnmount() {
      document.body.removeChild(this.el);
    }

    getDiv() {
      const div = document.createElement("div");
      const appendNode = this.props.appendNode || document.body;
      appendNode.appendChild(div);
      return div;
    }

    render() {
      return ReactDOM.createPortal(
        <WrappedComponent {...this.props} />,
        this.el
      );
    }
  }
  return AddPortal;
};

export default withPortal;

总结:高阶组件是一个函数,传递一个组件作为参数,并且会返回一个组件;在高阶组件中我们会对组件中公共的部分进行提取、抽象,可以对props,state进行控制,可以调用组件的生命周期方法,render方法,可以赋予组件一些具有共性的信息,但是我们不能对作为参数的组件进行修改,要保持他的纯函数的特性;通过对一系列组件的加工处理,可以大大提高我们的开发速度和销量,提高代码质量和组件易用性。

纯函数:如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数

tips:不要问我为什么不直接引用组件到高阶组件中,而不通过参数进行传递,如果这样的话,我们写每一个组件都要copy一份新的函数进行开发,何必呢