高阶组件

前言

Ract16后,想去官网看看有啥新的特性,无意间发现官网支持简体中文了,我是有多久没看了(以前学习害得我看繁体~~~),结果发现有些页面醉了,居然没翻译过来,好吧,只能弄自己蹩脚的英文翻译记录一下了。
我们都知道高阶函数, 高阶组件其实是差不多的用法,只不过传入的参数变成了React组件,并返回一个新的组件。

1、含义

官网的解释是:
A higher-order component is a function that takes a component and returns a new component.A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React's compositional nature.
(翻译:高阶组件是一个函数,它接受一个组件并返回一个新组件。高阶组件(HOC)是React中重用组件逻辑的一种高级技术。hoc本身不是React API的一部分,是由 React的组合特性而来的一种设计模式。)

2、实例

先举个🌰~~,一看就懂

 class commonCom extends React.Component {
        render() {
          console.log(this.props, "props");
          return <div>commonCom</div>;
        }
      }

      const simpleHoc = (WrappedComponent )=> {
        console.log("simpleHoc");
        return class extends React.Component {
          render() {
            return <WrappedComponent {...this.props} />;
          }
        };
      };
      const NewCom = simpleHoc(commonCom);//调用高阶组件函数
      let params = {
        id: 123,
        name: "kevin"
      };
      ReactDOM.render(
        <NewCom params={params} />,
        document.getElementById("root")
      );


上例中,组件commonCom 通过simpleHoc的包装,打了一个log。那么simpleHoc就是一个高阶组件了,通过接收一个组件class,并返回一个新的组件class。 在这个函数里,我们可以做很多操作。 而且return的组件同样有自己的生命周期和function。我们也可以把props传给WrappedComponent(被包装的组件),就像父组件给子组件传参一样
注意,HOC不会修改输入组件,也不会使用继承来复制其行为。相反,hoc通过将原始组件包装在容器组件中来组合它。hoc是一个零副作用的纯函数。

3、使用场景

有的时候,我们发现某些组件功能很类似,数据处理也是相同的,只是数据源和显示不同(举个官网的代码实例)

//组件A
class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource" is some global data source
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // Subscribe to changes
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // Clean up listener
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // Update component state whenever the data source changes
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

//组件B
class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id)
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id)
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}

上面代码中,CommentList和BlogPost它们的数据源不同并且他们的显示不同。但是我们从代码逻辑上来它们的大部分实现是相同的:
1.在构造器中添加数据的监听;
2.在监听数据方法中当数据改变时调用setState;
3.在组件卸载时移除数据监听。
可以想象,在一个大型应用程序中,上述情况出现的概率很大,订阅数据源和调用setstate的相同模式将反复发生。我们需要一个抽象的、在一个地方定义这个逻辑,并在许多组件之间共享它。这就是高阶组件的优势所在。
这样我们可以编写一个函数withSubscription来订阅数据源的组件,比如commentlist和blogpost。函数将接受作为其参数之一的子组件,该子组件将接收作为属性的订阅数据。

function withSubscription(WrappedComponent, selectData) {
  // ...and returns another component...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ... that takes care of the subscription...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... and renders the wrapped component with the fresh data!
      // Notice that we pass through any additional props
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

这个高阶组件,接受两个参数,第一个是被包装组件;第二个是获取数据的回调方法。改造一下上面的CommentList和BlogPost

//组件A
class CommentList extends React.Component {
  render() {
    return (
      <div>
        {this.props.data.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

//组件B
class BlogPost extends React.Component {
  render() {
    return <TextBlock text={this.props.data} />;
  }
}

上面实例中,被包装的组件CommentList和BlogPost接收容器的所有属性,以及一个新的属性数据,用于呈现其输出。HOC与数据的使用方式和使用原因无关,而封装组件与数据的来源无关。就这样,组件的功能更加的简洁和专注。
因为withSubscription是一个普通函数,所以您可以添加任意多个或任意少的参数。例如,您可能希望使dataprop的名称可配置,以进一步将hoc与包装的组件隔离开来。或者您可以接受配置shouldComponentUpdate的参数,或者接受配置数据源的参数。这些都是可能的,因为hoc可以完全控制如何定义组件。
另外,与组件一样,withsubscription和wrapped组件(上例中的CommentList和BlogPost)之间的契约完全基于props。这使得将一个hoc替换为另一个hoc变得容易,只要它们为被包装的组件提供相同的属性。如果更改数据提取库,不改变数据字段,只需修改withsubscription即可,作为数据展示的wrapped组件(上例中的CommentList和BlogPost)无需修改。

4、HOC的实现方式

4.1 属性代理(Props Proxy)的形式

通过HOc包装wrappedComponent,本来传给wrappedComponent的props,都在HOC中接受到了,也就是props proxy。 由此我们可以做一些操作:
一、操作props
最直观的就是接受到props,我们可以做任何读取,编辑,删除的很多自定义操作。包括hoc中定义的自定义事件,都可以通过props再传下去。

    const simpleHoc = WrappedComponent => {
        return class extends React.Component {
          render() {
            let mParams={"title":'new params'}; 
            return <WrappedComponent  { ...this.props}  mNewParams={mParams}/>;
          }
        };
      };

二、refs获取组件实例
当我们包装wrappedComponent的时候,想获取到它的实例怎么办,可以通过引用(ref),在wrappedComponent组件挂载的时候,会执行ref的回调函数,在hoc中取到组件的实例。这样它的props, state,方法都是可以取到的。可以参考我的另一篇关于《Refs和Refs 转发》的文章。这里就不举例子了,文章估计有点长~~~
三、抽离state
通过props传递回调函数,操作wrappedComponent组件的state.用的比较多的就是react处理表单的时候。通常react在处理表单的时候,一般使用的是受控组件,即把input都做成受控的,改变value的时候,用onChange事件同步到state中。

4.2 反向继承

反向继承(Inheritance Inversion),简称II。跟属性代理的方式不同的是,II采用通过 去继承WrappedComponent,本来是一种嵌套的关系,结果II返回的组件却继承了WrappedComponent,这看起来是一种反转的关系。
通过继承WrappedComponent,除了一些静态方法,包括生命周期,state,各种function,我们都可以得到。

 class commonCom extends React.Component {
        constructor() {
          super();
          this.state = {
            comName: "commonCom"
          };
        }

        componentDidMount() {
          console.log("didMount");
        }

        render() {
         // console.log("props:" + JSON.stringify(this.props));
          return <div>commonCom</div>;
        }
      }

const simpleHoc = WrappedComponent =>
        class extends WrappedComponent {
          render() {
            console.log("state:",this.state);//state: {comName: "commonCom"}
            return super.render();
          }
        };

simpleHoc return的组件通过继承,拥有了commonCom 的生命周期及属性,所以didMount会打印,state也通过constructor执行,得到state.comName。

5、注意点(Convention:惯例)

5.1 不要在HOCs中修改原组件原型(或对其进行其他修改)
function logProps(InputComponent) {
  InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
    console.log('Current props: ', this.props);
    console.log('Next props: ', nextProps);
  };
  // The fact that we're returning the original input is a hint that it has
  // been mutated.
  return InputComponent;
}

// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);

上例中,在高阶组件logProps中为修改了原组件InputComponent的原型方法componentWillReceiveProps,有的时候,根据不同的需求,一个组件在不同场景下需要不同的HOC进行包装,要是再有一个HOC组件也修改了原组件InputComponent的原型的属性或者方法,那么logProps修改的功能将会被覆盖。
所以,记住
HOCs won't work with function components, which do not have lifecycle methods.:
高阶组件不能用于没有生命周期方法的功能组件

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }
    render() {
      // Wraps the input component in a container, without mutating it. Good!
      return <WrappedComponent {...this.props} />;
    }
  }
}

改成上例的形式,既有上面的功能,同时避免了潜在的冲突。它是一个纯函数,所以它可以与其他hoc组合,甚至与自身组合。
有没有发现,HOCs和container components(容器组件)的模式相似。容器组件是分离高级别和低级别关注点之间责任的策略的一部分。容器管理订阅和状态等内容,并将属性传递给处理呈现UI等内容的组件。HOCs使用容器作为其实现的一部分。您可以将hocs视为参数化容器组件定义。

5.2 传递参数

HOCs向组件添加参数时,不要把和自己相关的参数传递过去,另外自己注入的参数要和外部传递给包装组件的分开。

render() {
  // Filter out extra props that are specific to this HOC and shouldn't be
  // passed through
  const { extraProp, ...passThroughProps } = this.props;

  // Inject props into the wrapped component. These are usually state values or
  // instance methods.
  const injectedProp = someStateOrInstanceMethod;

  // Pass props to wrapped component
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

这个惯例有助于确保HOCs尽可能灵活和可重用。

5.3 不要再render方法中使用HOCs

react的diffing算法(称为和解)使用组件标识来确定它是应该更新现有的子树,还是丢弃它并装载一个新的子树。如果从render返回的组件与上一个render返回的组件相同(===),那么react会通过将其与新的组件进行比较来递归更新子树。如果它们不相等,则会完全卸载前一个子树。

render() {
  // A new version of EnhancedComponent is created on every render
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // That causes the entire subtree to unmount/remount each time!
  return <EnhancedComponent />;
}

这里的问题不仅仅是性能问题——重新安装组件会导致该组件及其所有子组件的状态丢失。相反,在组件定义之外应用hocs,以便生成的组件只创建一次。然后,它的身份将在渲染中保持一致。在那些需要动态应用hoc的罕见情况下,您也可以在组件的生命周期方法或其构造函数内进行。

5.4 复制被包装组件的静态方法

将hoc应用于组件时,原始组件将被容器组件包装。这意味着新组件没有原始组件的任何静态方法。

// Define a static method
WrappedComponent.staticMethod = function() {/*...*/}
// Now apply a HOC
const EnhancedComponent = enhance(WrappedComponent);

// The enhanced component has no static method
typeof EnhancedComponent.staticMethod === 'undefined' // true

要解决此问题,可以在返回容器之前将方法复制到容器上。(不推荐)

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // Must know exactly which method(s) to copy :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}

那么问题来了,我们就需要确切地知道需要复制哪些方法。这时,我们可以使用hoist-non-react-statics,自动复制所有non-React 的静态方法。(react-router 里withRouter就使用了这个包)

import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}

另一种可能的解决方案是将静态方法与组件本身分开导出

// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...export the method separately...
export { someFunction };

// ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';
5.5 最大化可组合性

并非所有的HOCs都是一样的。有时,它们只接受一个参数,即封装的组件。

const NavbarWithRouter = withRouter(Navbar);

通常,hocs接受额外的参数。

const CommentWithRelay = Relay.createContainer(Comment, config);

上例中,Relay中的高阶组件依赖于一个配置对象config。
最常见的形式类似于Redux中的connect方法

// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);

感觉晕,看不懂?那我们就拆开看看

// connect is a function that returns another function
const enhance = connect(commentListSelector, commentListActions);
// The returned function is a HOC, which returns a component that is connected
// to the Redux store
const ConnectedComment = enhance(CommentList);

这样就很明显,connect方法返回的是个函数。换句话说,connect函数是一个返回高阶组件的高阶函数!
这种形式可能看起来很混乱,但它有一个有用的特性,像connect函数返回的单参数hoc具有 Component => Component的特征。输出类型与输入类型相同的函数很容易组合在一起。

// Instead of doing this...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// ... you can use a function composition utility
// compose(f, g, h) is the same as (...args) => f(g(h(...args)))
const enhance = compose(
  // These are both single-argument HOCs
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)

(同样的属性也允许将connect和其他增强型hoc用作装饰器,这是一个实验性的javascript提议)
compose实用程序功能由许多第三方库提供,包括lodash(作为lodash.flowright)、redux和ramda。

5.6 Refs 穿透不到被包装的组件

虽然高阶组件的约定是将所有属性传递给封装的组件,但这对refs不起作用。这是因为refis不是真正的props,它类似于key,由react专门处理的。如果向其组件是HOC组件的元素添加引用,则引用的是最外层容器组件的实例,而不是包装组件。可以使用react.forwardRef API。可以参考我的另一篇关于《Refs和Refs 转发》的文章。

6、总结

高阶组件最大的好处就是解耦和灵活性,在react的开发中还是很有用的。这里只是按照官网以及自己的理解记录。掌握它的技巧,了解它的限制,避开它的缺点,结合自己的应用场景,灵活运用,会有意想不到的效果。
(多多包涵,尽量用通俗的话写了,各种翻译软件,总觉得有些名词翻译的有问题~~~o(∩_∩)o 哈哈~~~)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343