React进阶初探01--高阶组件

251 阅读6分钟

高阶组件HOC概念

基本定义

  • 形式上是一个函数

  • 功能:输入一个原组件(以及某些可能会用到的其他数据),输出一个将该组件进行一定包装后的新组件。且不会影响原组件

  • 是一个“工厂factory”

  • 理解 【包装】 概念

    • 属性代理:操控传递给原组件(被包装组件)的props
    • 反向继承:高阶组件“继承”了被包装组件

高阶组件认知

  • 一个简单的例子:

    • 高阶组件withSubscription
    • 两个要包装的组件 CommentListBlogPost
    • 功能:两个组件需要从远程获取最新数据并保持更新的功能,且实现方式相似,使用高阶组件进行封装,将他们的这种相似行为抽离出来
  • 先看调用高阶组件的地方如何传值

//传入的第一个参数为组件本身,第二个参数为一个函数
//DataSource是一个全局变量,只要知道他里面存储着需要的数据,通过内部的getxx方法能拿到数据即可。
//props就是组件的props,后面详说
const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

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

  • 高阶组件withSubscription的实现
    1. 根据前面说的,函数的返回值是一个新的组件
    2. 首先通过传入的函数selectData和组件构造函数接收的props获取到所需的数据,存入statedata属性。(由于JS的特性,就算传入比形参多的参数也不会有什么问题)
    3. 实现一个handleChange方法,用于在数据发生改变时获取并赋值
    4. 在组件挂载和销毁时分别调用DataSource自带的添加监听和移除监听方法,这是DataSource用来检查数据的,不用关心,只要知道会让从远程获取的数据保持最新就行。
    5. 最后在render中return传入的组件,
      1. 并将获取的数据传递给组件。
      2. 同时还要把其他props也传递回去(这种写法相当于把所有props拆出来一个一个写成data1=props.data1 data2=props.data2的形式)
// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
  // ...并返回另一个组件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ...负责订阅相关的操作...
      DataSource.addChangeListener(this.handleChange);
    }

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

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

    render() {
      // ... 并使用新数据渲染被包装的组件!
      // 请注意,我们可能还会传递其他属性
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}
  • 特性
    • 高阶组件是一个纯函数,即他不会改变传入的参数,返回的是一个全新的独立的组件
    • **由于高阶组件是一个自定义的函数,所以其参数数目并不是固定的。开发者可以根据需要随意更改实际函数的参数。**比如想要从外部配置上面例子中传给被包装组件的data属性名,就可以在高阶组件函数的参数中增加第三个参数作为data传入被包装组件时的名字

规范化和约定

规范化:使用组合而不要改变原组件

  • 错误用法:
function logProps(InputComponent) {
  InputComponent.prototype.componentDidUpdate = function(prevProps) {
    console.log('Current props: ', this.props);
    console.log('Previous props: ', prevProps);
  };
  // 返回原始的 input 组件,暗示它已经被修改。
  return InputComponent;
}

// 每次调用 logProps 时,增强组件都会有 log 输出。
const EnhancedComponent = logProps(InputComponent);

如上面对原组件进行了修改,如果现在又有第二个高阶组件去处理这个组件并修改componentDidUpdate钩子的内容,则这次logProps中的修改又被第二个高阶组件覆盖。这样下去结果只有最后一个被调用的高阶组件会确保成功修改,造成混乱。另外,这样的修改会导致你再也无法使用原本的组件。

  • 正确用法:使用组合,封装一个新的组件,实现对钩子函数的修改
    • 之后如果要对原组件钩子函数内做其他功能就用原组件传入新的高阶组件
    • 如果要在此基础上继续修改钩子函数就以这里生成的新组建传入新的高阶函数
function logProps(WrappedComponent) {
  return class extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('Current props: ', this.props);
      console.log('Previous props: ', prevProps);
    }
    render() {
      // 将 input 组件包装在容器中,而不对其进行修改。Good!
      return <WrappedComponent {...this.props} />;
    }
  }
}

约定:关于porps透传的约定

文档中关于这部分略微有些模糊,给的例子代码也相当抽象,网上找到的解释感觉也不是很详细,几乎都是照抄了文档。

  • 个人理解
    • HOC应该做到“==透明==”,即经过这层包装的新组件在实际功能上并==没有变化==,改变的只是功能的实现方式
    • 对于HOC没用到而原组件需要的porps,应该直接==原封不动传过去==
    • 对于原组件用不到的外部额外传来的porps,==不要传==给原组件
    • 对于HOC内实现的新方法和新属性(比如前面例子中从远程获取到的数据),额外传递给原组件

约定:最大化可组合性

  • 核心观点是 要活用HOC的函数特性。个人理解其目的是要增大复合使用时的易用性。
  • React Reduxconnect方法为例。(不知道也没关系,只要知道它是一个函数,而它的返回值是一个只接收组件作为参数的高阶组件——也是一个函数。即connect是一个高阶函数)
    • HOC的特点:传入【组件】=>返回【组件】
      • ps:中文文档这里说是“签名”,但我觉得signature应该翻译为特征。。。吧?
    • 意义:多层嵌套执行。比如要经过多层封装,可以像这样去调用
f(g(h(...args)))

约定:包装显示名称

  • 主要是为了调试方便,设定displayName
function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

规范:不要在render中使用HOC

  • React的更新策略会比较新旧子树
    • 新旧组件相等(===),则比较子树之间不同的地方进行更新
    • 否则,丢弃旧子树,使用新子树
  • 因此,如果在render中使用HOC,则不仅每次都要调用HOC重新创建,还注定每次比较结果都是不同,即每次都要卸载旧子树使用新子树
  • 这样除了==浪费性能==外,还会导致旧子树及其后续的子节点==状态丢失==(因为重新创建全新的了)

规范:复制静态方法

  • 如果组件内实现了静态方法,则在HOC中必须主动将他们拷贝到封装后的组件中,否则他们会无法调用
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // 必须准确知道应该拷贝哪些方法 
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance; //Enhance就是包装好的组件
}

上面的缺点是必须知道有哪些方法要复制,麻烦而且容易出错。当然可以用前面说的,给HOC函数添加参数,通过参数传递所有需要复制的静态方法的名称,循环拷贝

  • 使用 hoistNonReactStatic自动拷贝
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}
  • 或者将这些方法额外导出
// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...单独导出该方法...
export { someFunction };

// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from './MyComponent.js';

注意:Refs不会被传递

  • refskey等都不算在props内,因此无法通过props传递给原组件
  • 解决办法是React.forwardRef,通过其他方式获取和传递,具体说明在官方文档Ref章节