# 从 mixin 到 hoc 再到 hook-2

# 高阶组件(HOC)

image

高阶组件可以看作React对装饰模式的一种实现,高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。

高阶组件(HOC)是React中的高级技术,用来重用组件逻辑。但高阶组件本身并不是React API。它只是一种模式,这种模式是由React自身的组合性质必然产生的。

function visible(WrappedComponent) {
  return class extends Component {
    render() {
      const { visible, ...props } = this.props;
      if (visible === false) return null;
      return <WrappedComponent {...props} />;
    }
  };
}

上面的代码就是一个HOC的简单应用,函数接收一个组件作为参数,并返回一个新组件,新组建可以接收一个visible props,根据visible的值来判断是否渲染 Visible。

下面我们从以下几方面来具体探索HOC

image

# HOC 的实现方式

# 属性代理

函数返回一个我们自己定义的组件,然后在render中返回要包裹的组件,这样我们就可以代理所有传入的props,并且决定如何渲染,实际上 ,这种方式生成的高阶组件就是原组件的父组件,上面的函数visible就是一个HOC属性代理的实现方式。

function proxyHOC(WrappedComponent) {
  return class extends Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

对比原生组件增强的项:

  • 可操作所有传入的props
  • 可操作组件的生命周期
  • 可操作组件的static方法
  • 获取refs

# 反向继承

返回一个组件,继承原组件,在render中调用原组件的render。由于继承了原组件,能通过 this 访问到原组件的生命周期、props、state、render等,相比属性代理它能操作更多的属性。

function inheritHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      return super.render();
    }
  };
}

对比原生组件增强的项:

  • 可操作所有传入的props
  • 可操作组件的生命周期
  • 可操作组件的static方法
  • 获取refs
  • 可操作state
  • 可以渲染劫持

# HOC 可以实现什么功能

# 组合渲染

可使用任何其他组件和原组件进行组合渲染,达到样式、布局复用等效果。

通过属性代理实现

function stylHOC(WrappedComponent) {
  return class extends Component {
    render() {
      return (
        <div>
          <div className="title">{this.props.title}</div>
          <WrappedComponent {...this.props} />
        </div>
      );
    }
  };
}

通过反向继承实现

function styleHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      return (
        <div>
          <div className="title">{this.props.title}</div>
          {super.render()}
        </div>
      );
    }
  };
}

# 条件渲染

根据特定的属性决定原组件是否渲染

通过属性代理实现

function visibleHOC(WrappedComponent) {
  return class extends Component {
    render() {
      if (this.props.visible === false) return null;
      return <WrappedComponent {...props} />;
    }
  };
}

通过反向继承实现

function visibleHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      if (this.props.visible === false) {
        return null;
      } else {
        return super.render();
      }
    }
  };
}

# 操作 props

可以对传入组件的props进行增加、修改、删除或者根据特定的props进行特殊的操作。

通过属性代理实现

function proxyHOC(WrappedComponent) {
  return class extends Component {
    render() {
      const newProps = {
        ...this.props,
        user: "ConardLi",
      };
      return <WrappedComponent {...newProps} />;
    }
  };
}

# 获取 refs

高阶组件中可获取原组件的ref,通过ref获取组件实力,如下面的代码,当程序初始化完成后调用原组件的 log 方法。(不知道 refs 怎么用,请 👇Refs & DOM (opens new window))

通过属性代理实现

function refHOC(WrappedComponent) {
  return class extends Component {
    componentDidMount() {
      this.wapperRef.log();
    }
    render() {
      return (
        <WrappedComponent
          {...this.props}
          ref={(ref) => {
            this.wapperRef = ref;
          }}
        />
      );
    }
  };
}

这里注意:调用高阶组件的时候并不能获取到原组件的真实ref,需要手动进行传递,具体请看传递 refs (opens new window)

# 状态管理

将原组件的状态提取到HOC中进行管理,如下面的代码,我们将Inputvalue提取到HOC中进行管理,使它变成受控组件,同时不影响它使用onChange方法进行一些其他操作。基于这种方式,我们可以实现一个简单的双向绑定,具体请看双向绑定 (opens new window)

通过属性代理实现

function proxyHoc(WrappedComponent) {
  return class extends Component {
    constructor(props) {
      super(props);
      this.state = { value: "" };
    }

    onChange = (event) => {
      const { onChange } = this.props;
      this.setState(
        {
          value: event.target.value,
        },
        () => {
          if (typeof onChange === "function") {
            onChange(event);
          }
        }
      );
    };

    render() {
      const newProps = {
        value: this.state.value,
        onChange: this.onChange,
      };
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  };
}

class HOC extends Component {
  render() {
    return <input {...this.props}></input>;
  }
}

export default proxyHoc(HOC);

# 操作 state

上面的例子通过属性代理利用 HOC 的 state 对原组件进行了一定的增强,但并不能直接控制原组件的state,而通过反向继承,我们可以直接操作原组件的state。但是并不推荐直接修改或添加原组件的state,因为这样有可能和组件内部的操作构成冲突。

通过反向继承实现

function debugHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      console.log("props", this.props);
      console.log("state", this.state);
      return <div className="debuging">{super.render()}</div>;
    }
  };
}

上面的HOCrender中将propsstate打印出来,可以用作调试阶段,当然你可以在里面写更多的调试代码。想象一下,只需要在我们想要调试的组件上加上@debug就可以对该组件进行调试,而不需要在每次调试的时候写很多冗余代码。(如果你还不知道怎么使用 HOC,请 👇如何使用 HOC (opens new window))

# 渲染劫持

高阶组件可以在 render 函数中做非常多的操作,从而控制原组件的渲染输出。只要改变了原组件的渲染,我们都将它称之为一种渲染劫持

实际上,上面的组合渲染 (opens new window)条件渲染 (opens new window)都是渲染劫持的一种,通过反向继承,不仅可以实现以上两点,还可直接增强由原组件render函数产生的React元素

通过反向继承实现

function hijackHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      const tree = super.render();
      let newProps = {};
      if (tree && tree.type === "input") {
        newProps = { value: "渲染被劫持了" };
      }
      const props = Object.assign({}, tree.props, newProps);
      const newTree = React.cloneElement(tree, props, tree.props.children);
      return newTree;
    }
  };
}

注意上面的说明我用的是增强而不是更改render函数内实际上是调用React.creatElement产生的React元素

image虽然我们能拿到它,但是我们不能直接修改它里面的属性,我们通过getOwnPropertyDescriptors函数来打印下它的配置项:

image

可以发现,所有的writable属性均被配置为了false,即所有属性是不可变的。(对这些配置项有疑问,请 👇defineProperty (opens new window)

不能直接修改,我们可以借助cloneElement方法来在原组件的基础上增强一个新组件:

React.cloneElement()克隆并返回一个新的React元素,使用element作为起点。生成的元素将会拥有原始元素 props 与新 props 的浅合并。新的子级会替换现有的子级。来自原始元素的 key 和 ref 将会保留。

React.cloneElement()几乎相当于:

<element.type {...element.props} {...props}>
  {children}
</element.type>

# 如何使用 HOC

上面的示例代码都写的是如何声明一个HOCHOC实际上是一个函数,所以我们将要增强的组件作为参数调用HOC函数,得到增强后的组件。

class myComponent extends Component {
  render() {
    return <span>原组件</span>;
  }
}
export default inheritHOC(myComponent);

# compose

在实际应用中,一个组件可能被多个HOC增强,我们使用的是被所有的HOC增强后的组件,借用一张装饰模式的图来说明,可能更容易理解:

image

假设现在我们有loggervisiblestyle等多个HOC,现在要同时增强一个Input组件:

logger(visible(style(Input)));

这种代码非常的难以阅读,我们可以手动封装一个简单的函数组合工具,将写法改写如下:

const compose = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args)));
compose(logger, visible, style)(Input);

compose函数返回一个所有函数组合后的函数,compose(f, g, h)(...args) => f(g(h(...args)))是一样的。

很多第三方库都提供了类似compose的函数,例如lodash.flowRightRedux提供的combineReducers函数等。

# Decorators

我们还可以借助ES7为我们提供的Decorators来让我们的写法变的更加优雅:

@logger
@visible
@style
class Input extends Component {
  // ...
}

DecoratorsES7的一个提案,还没有被标准化,但目前Babel转码器已经支持,我们需要提前配置babel-plugin-transform-decorators-legacy

"plugins": ["transform-decorators-legacy"]

还可以结合上面的compose函数使用:

const hoc = compose(logger, visible, style);
@hoc
class Input extends Component {
  // ...
}

文中如有错误,欢迎在评论区指正,谢谢阅读。

Last Updated: 8/4/2019, 10:35:29 AM

从 Mixin 到 HOC 再到 Hook(一) (opens new window)从 Mixin 到 HOC 再到 Hook(三) (opens new window)

上次更新: 11/8/2024, 10:19:43 AM