React组件复用的方式

React组件复用的方式

现前端的工程化愈加重要,虽然使用Ctrl+CCtrl+V一样可以完成需求,可是一旦面临修改那就是一项庞大的任务,因而减小代码的拷贝,增长封装复用能力,实现可维护、可复用的代码就变得尤其重要,在React中组件是代码复用的主要单元,基于组合的组件复用机制至关优雅,而对于更细粒度的逻辑(状态逻辑、行为逻辑等),复用起来却不那么容易,很难把状态逻辑拆出来做为一个可复用的函数或组件,实际上在Hooks出现以前,都缺乏一种简单直接的组件行为扩展方式,对于MixinHOCRender Props都算是在既有(组件机制的)游戏规则下探索出来的上层模式,一直没有从根源上很好地解决组件间逻辑复用的问题,直到Hooks登上舞台,下面咱们就来介绍一下MixinHOCRender PropsHooks四种组件间复用的方式。html

Mixin

固然React好久以前就再也不建议使用Mixin做为复用的解决方案,可是如今依旧能经过create-react-class提供对Mixin的支持,此外注意在以ES6class方式声明组件时是不支持Mixin的。
Mixins容许多个React组件之间共享代码,它们很是相似于Python中的mixinsPHP中的traitsMixin方案的出现源自一种OOP直觉,只在早期提供了React.createClass() API(React v15.5.0正式废弃,移至create-react-class)来定义组件,天然而然地,(类)继承就成了一种直觉性的尝试,而在JavaScript基于原型的扩展模式下,相似于继承的Mixin方案就成了一个不错的解决方案,Mixin主要用来解决生命周期逻辑和状态逻辑的复用问题,容许从外部扩展组件生命周期,在Flux等模式中尤其重要,可是在不断实践中也出现了不少缺陷:前端

  • 组件与Mixin之间存在隐式依赖(Mixin常常依赖组件的特定方法,但在定义组件时并不知道这种依赖关系)。
  • 多个Mixin之间可能产生冲突(好比定义了相同的state字段)。
  • Mixin倾向于增长更多状态,这下降了应用的可预测性,致使复杂度剧增。
  • 隐式依赖致使依赖关系不透明,维护成本和理解成本迅速攀升。
  • 难以快速理解组件行为,须要全盘了解全部依赖Mixin的扩展行为,及其之间的相互影响
  • 组件自身的方法和state字段不敢轻易删改,由于难以肯定有没有Mixin依赖它。
  • Mixin也难以维护,由于Mixin逻辑最后会被打平合并到一块儿,很难搞清楚一个Mixin的输入输出。

毫无疑问,这些问题是致命的,因此,Reactv0.13.0放弃了Mixin静态横切(相似于继承的复用),转而走向HOC高阶组件(相似于组合的复用)。react

示例

上古版本示例,一个通用的场景是: 一个组件须要按期更新,用setInterval()作很容易,但当不须要它的时候取消定时器来节省内存是很是重要的,React提供生命周期方法来告知组件建立或销毁的时间,下面的Mixin,使用setInterval()并保证在组件销毁时清理定时器。git

var SetIntervalMixin = {
  componentWillMount: function() {
    this.intervals = [];
  },
  setInterval: function() {
    this.intervals.push(setInterval.apply(null, arguments));
  },
  componentWillUnmount: function() {
    this.intervals.forEach(clearInterval);
  }
};

var TickTock = React.createClass({
  mixins: [SetIntervalMixin], // 引用 mixin
  getInitialState: function() {
    return {seconds: 0};
  },
  componentDidMount: function() {
    this.setInterval(this.tick, 1000); // 调用 mixin 的方法
  },
  tick: function() {
    this.setState({seconds: this.state.seconds + 1});
  },
  render: function() {
    return (
      <p>
        React has been running for {this.state.seconds} seconds.
      </p>
    );
  }
});

ReactDOM.render(
  <TickTock />,
  document.getElementById("example")
);

HOC

Mixin以后,HOC高阶组件担起重任,成为组件间逻辑复用的推荐方案,高阶组件从名字上就透漏出高级的气息,实际上这个概念应该是源自于JavaScript的高阶函数,高阶函数就是接受函数做为输入或者输出的函数,能够想到柯里化就是一种高阶函数,一样在React文档上也给出了高阶组件的定义,高阶组件是接收组件并返回新组件的函数。具体的意思就是: 高阶组件能够看做React对装饰模式的一种实现,高阶组件就是一个函数,且该函数接受一个组件做为参数,并返回一个新的组件,他会返回一个加强的React组件,高阶组件可让咱们的代码更具备复用性,逻辑性与抽象性,能够对render方法进行劫持,也能够控制propsstate等。
对比MixinHOCMixin是一种混入的模式,在实际使用中Mixin的做用仍是很是强大的,可以使得咱们在多个组件中共用相同的方法,但一样也会给组件不断增长新的方法和属性,组件自己不只能够感知,甚至须要作相关的处理(例如命名冲突、状态维护等),一旦混入的模块变多时,整个组件就变的难以维护,Mixin可能会引入不可见的属性,例如在渲染组件中使用Mixin方法,给组件带来了不可见的属性props和状态state,而且Mixin可能会相互依赖,相互耦合,不利于代码维护,此外不一样的Mixin中的方法可能会相互冲突。以前React官方建议使用Mixin用于解决横切关注点相关的问题,但因为使用Mixin可能会产生更多麻烦,因此官方如今推荐使用HOC。高阶组件HOC属于函数式编程functional programming思想,对于被包裹的组件时不会感知到高阶组件的存在,而高阶组件返回的组件会在原来的组件之上具备功能加强的效果,基于此React官方推荐使用高阶组件。
HOC虽然没有那么多致命问题,但也存在一些小缺陷:github

  • 扩展性限制: HOC并不能彻底替代Mixin,一些场景下,Mixin能够而HOC作不到,好比PureRenderMixin,由于HOC没法从外部访问子组件的State,同时经过shouldComponentUpdate滤掉没必要要的更新,所以,React在支持ES6Class以后提供了React.PureComponent来解决这个问题。
  • Ref传递问题: Ref被隔断,Ref的传递问题在层层包装下至关恼人,函数Ref可以缓解一部分(让HOC得以获知节点建立与销毁),以至于后来有了React.forwardRef API
  • WrapperHell: HOC泛滥,出现WrapperHell(没有包一层解决不了的问题,若是有,那就包两层),多层抽象一样增长了复杂度和理解成本,这是最关键的缺陷,而HOC模式下没有很好的解决办法。

示例

具体而言,高阶组件是参数为组件,返回值为新组件的函数,组件是将props转换为UI,而高阶组件是将组件转换为另外一个组件。HOCReact的第三方库中很常见,例如ReduxconnectRelaycreateFragmentContainer算法

// 高阶组件定义
const higherOrderComponent = (WrappedComponent) => {
    return class EnhancedComponent extends React.Component {
        // ...
        render() {
          return <WrappedComponent {...this.props} />;
        }
  };
}

// 普通组件定义
class WrappedComponent extends React.Component{
    render(){
        //....
    }
}

// 返回被高阶组件包装过的加强组件
const EnhancedComponent = higherOrderComponent(WrappedComponent);

在这里要注意,不要试图以任何方式在HOC中修改组件原型,而应该使用组合的方式,经过将组件包装在容器组件中实现功能。一般状况下,实现高阶组件的方式有如下两种:编程

  • 属性代理Props Proxy
  • 反向继承Inheritance Inversion

属性代理

例如咱们能够为传入的组件增长一个存储中的id属性值,经过高阶组件咱们就能够为这个组件新增一个props,固然咱们也能够对在JSX中的WrappedComponent组件中props进行操做,注意不是操做传入的WrappedComponent类,咱们不该该直接修改传入的组件,而能够在组合的过程当中对其操做。数组

const HOC = (WrappedComponent, store) => {
    return class EnhancedComponent extends React.Component {
        render() {
            const newProps = {
                id: store.id
            }
            return <WrappedComponent
                {...this.props}
                {...newProps}
            />;
        }
    }
}

咱们也能够利用高阶组件将新组件的状态装入到被包装组件中,例如咱们可使用高阶组件将非受控组件转化为受控组件。性能优化

class WrappedComponent extends React.Component {
    render() {
        return <input name="name" />;
    }
}

const HOC = (WrappedComponent) => {
    return class EnhancedComponent extends React.Component {
        constructor(props) {
            super(props);
            this.state = { name: "" };
        }
        render() {
            const newProps = {
                value: this.state.name,
                onChange: e => this.setState({name: e.target.value}),
            }
            return <WrappedComponent 
                {...this.props} 
                {...newProps} 
            />;
        }
    }
}

或者咱们的目的是将其使用其余组件包裹起来用以达成布局或者是样式的目的。babel

const HOC = (WrappedComponent) => {
    return class EnhancedComponent extends React.Component {
        render() {
            return (
                <div class="layout">
                    <WrappedComponent  {...this.props} />
                </div>
            );
        }
    }
}

反向继承

反向继承是指返回的组件去继承以前的组件,在反向继承中咱们能够作很是多的操做,修改stateprops甚至是翻转Element Tree,反向继承有一个重要的点,反向继承不能保证完整的子组件树被解析,也就是说解析的元素树中包含了组件(函数类型或者Class类型),就不能再操做组件的子组件了。
当咱们使用反向继承实现高阶组件的时候能够经过渲染劫持来控制渲染,具体是指咱们能够有意识地控制WrappedComponent的渲染过程,从而控制渲染控制的结果,例如咱们能够根据部分参数去决定是否渲染组件。

const HOC = (WrappedComponent) => {
    return class EnhancedComponent extends WrappedComponent {
        render() {
            return this.props.isRender && super.render();  
        }
    }
}

甚至咱们能够经过重写的方式劫持原组件的生命周期。

const HOC = (WrappedComponent) => {
    return class EnhancedComponent extends WrappedComponent {
        componentDidMount(){
          // ...
        }
        render() {
            return super.render();  
        }
    }
}

因为其实是继承关系,咱们能够去读取组件的propsstate,若是有必要的话,甚至能够修改增长、修改和删除propsstate,固然前提是修改带来的风险须要你本身来控制。在一些状况下,咱们可能须要为高阶属性传入一些参数,那咱们就能够经过柯里化的形式传入参数,配合高阶组件能够完成对组件的相似于闭包的操做。

const HOCFactoryFactory = (params) => {
    // 此处操做params
    return (WrappedComponent) => {
        return class EnhancedComponent extends WrappedComponent {
            render() {
                return params.isRender && this.props.isRender && super.render();
            }
        }
    }
}

注意

不要改变原始组件

不要试图在HOC中修改组件原型,或以其余方式改变它。

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);

这样作会产生一些不良后果,其一是输入组件再也没法像HOC加强以前那样使用了,更严重的是,若是你再用另外一个一样会修改componentDidUpdateHOC加强它,那么前面的HOC就会失效,同时这个HOC也没法应用于没有生命周期的函数组件。
修改传入组件的HOC是一种糟糕的抽象方式,调用者必须知道他们是如何实现的,以免与其余HOC发生冲突。HOC不该该修改传入组件,而应该使用组合的方式,经过将组件包装在容器组件中实现功能。

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentDidUpdate(prevProps) {
      console.log("Current props: ", this.props);
      console.log("Previous props: ", prevProps);
    }
    render() {
      // 将 input 组件包装在容器中,而不对其进行修改,Nice!
      return <WrappedComponent {...this.props} />;
    }
  }
}

过滤props

HOC为组件添加特性,自身不该该大幅改变约定,HOC返回的组件与原组件应保持相似的接口。HOC应该透传与自身无关的props,大多数HOC都应该包含一个相似于下面的render方法。

render() {
  // 过滤掉额外的 props,且不要进行透传
  const { extraProp, ...passThroughProps } = this.props;

  // 将 props 注入到被包装的组件中。
  // 一般为 state 的值或者实例方法。
  const injectedProp = someStateOrInstanceMethod;

  // 将 props 传递给被包装组件
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

最大化可组合性

并非全部的HOC都同样,有时候它仅接受一个参数,也就是被包裹的组件。

const NavbarWithRouter = withRouter(Navbar);

HOC一般能够接收多个参数,好比在RelayHOC额外接收了一个配置对象用于指定组件的数据依赖。

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

最多见的HOC签名以下,connect是一个返回高阶组件的高阶函数。

// React Redux 的 `connect` 函数
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);

// connect 是一个函数,它的返回值为另一个函数。
const enhance = connect(commentListSelector, commentListActions);
// 返回值为 HOC,它会返回已经链接 Redux store 的组件
const ConnectedComment = enhance(CommentList);

这种形式可能看起来使人困惑或没必要要,但它有一个有用的属性,像connect函数返回的单参数HOC具备签名Component => Component,输出类型与输入类型相同的函数很容易组合在一块儿。一样的属性也容许connect和其余HOC承担装饰器的角色。此外许多第三方库都提供了compose工具函数,包括lodashReduxRamda

const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// 你能够编写组合工具函数
// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
const enhance = compose(
  // 这些都是单参数的 HOC
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)

不要在render方法中使用HOC

Reactdiff算法使用组件标识来肯定它是应该更新现有子树仍是将其丢弃并挂载新子树,若是从render返回的组件与前一个渲染中的组件相同===,则React经过将子树与新子树进行区分来递归更新子树,若是它们不相等,则彻底卸载前一个子树。
一般在使用的时候不须要考虑这点,但对HOC来讲这一点很重要,由于这表明着你不该在组件的render方法中对一个组件应用HOC

render() {
  // 每次调用 render 函数都会建立一个新的 EnhancedComponent
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // 这将致使子树每次渲染都会进行卸载,和从新挂载的操做!
  return <EnhancedComponent />;
}

这不只仅是性能问题,从新挂载组件会致使该组件及其全部子组件的状态丢失,若是在组件以外建立HOC,这样一来组件只会建立一次。所以每次render时都会是同一个组件,通常来讲,这跟你的预期表现是一致的。在极少数状况下,你须要动态调用HOC,你能够在组件的生命周期方法或其构造函数中进行调用。

务必复制静态方法

有时在React组件上定义静态方法颇有用,例如Relay容器暴露了一个静态方法getFragment以方便组合GraphQL片断。可是当你将HOC应用于组件时,原始组件将使用容器组件进行包装,这意味着新组件没有原始组件的任何静态方法。

// 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 如今使用 HOC
const EnhancedComponent = enhance(WrappedComponent);

// 加强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === "undefined" // true

为了解决这个问题,你能够在返回以前把这些方法拷贝到容器组件上。

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // 必须准确知道应该拷贝哪些方法 :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}

但要这样作,你须要知道哪些方法应该被拷贝,你可使用hoist-non-react-statics依赖自动拷贝全部非React静态方法。

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不会被传递

虽然高阶组件的约定是将全部props传递给被包装组件,但这对于refs并不适用,那是由于ref实际上并非一个prop,就像key同样,它是由React专门处理的。若是将ref添加到HOC的返回组件中,则ref引用指向容器组件,而不是被包装组件,这个问题能够经过React.forwardRef这个API明确地将refs转发到内部的组件。。

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      const {forwardedRef, ...rest} = this.props;

      // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // 注意 React.forwardRef 回调的第二个参数 “ref”。
  // 咱们能够将其做为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
  // 而后它就能够被挂载到被 LogProps 包裹的子组件上。
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

Render Props

HOC同样,Render Props也是一直以来都存在的元老级模式,render props指在一种React组件之间使用一个值为函数的props共享代码的简单技术,具备render props的组件接收一个函数,该函数返回一个React元素并调用它而不是实现一个本身的渲染逻辑,render props是一个用于告知组件须要渲染什么内容的函数props,也是组件逻辑复用的一种实现方式,简单来讲就是在被复用的组件中,经过一个名为render(属性名也能够不是render,只要值是一个函数便可)的prop属性,该属性是一个函数,这个函数接受一个对象并返回一个子组件,会将这个函数参数中的对象做为props传入给新生成的组件,而在使用调用者组件这里,只须要决定这个组件在哪里渲染以及该以何种逻辑渲染并传入相关对象便可。
对比HOCRender Props,技术上,两者都基于组件组合机制,Render Props拥有与HOC 同样的扩展能力,称之为Render Props,并非说只能用来复用渲染逻辑,而是表示在这种模式下,组件是经过render()组合起来的,相似于HOC 模式下经过Wrapperrender()创建组合关系形式上,两者很是相像,一样都会产生一层Wrapper,而实际上Render Props HOC 甚至可以相互转换。
一样,Render Props也会存在一些问题:

  • 数据流向更直观了,子孙组件能够很明确地看到数据来源,但本质上Render Props是基于闭包实现的,大量地用于组件的复用将不可避免地引入了callback hell问题。
  • 丢失了组件的上下文,所以没有this.props属性,不能像HOC那样访问this.props.children

示例

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <title>React</title>
</head>

<body>
    <div id="root"></div>
</body>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
class MouseTracker extends React.Component {
  constructor(props) {
    super(props);
    this.state = { x: 0,  y: 0, }
  }
  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }
  render() {
    return (
      <div onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)} {/* Render Props */}
      </div>
    )
  }
}

class MouseLocation extends React.Component {
  render() {
    return (
      <>
        <h1>请在此处移动鼠标</h1>
        <p>当前鼠标的位置是: x:{this.props.mouse.x} y:{this.props.mouse.y}</p>
      </>
    )
  }
}

ReactDOM.render(
  <MouseTracker render={mouse => <MouseLocation mouse={mouse} />}></MouseTracker>, 
  document.getElementById("root")
);
</script>

</html>

Hooks

代码复用的解决方案层出不穷,可是总体来讲代码复用仍是很复杂的,这其中很大一部分缘由在于细粒度代码复用不该该与组件复用捆绑在一块儿,HOCRender Props 等基于组件组合的方案,至关于先把要复用的逻辑包装成组件,再利用组件复用机制实现逻辑复用,天然就受限于组件复用,于是出现扩展能力受限、Ref 隔断、Wrapper Hell等问题,那么咱们就须要有一种简单直接的代码复用方式,函数,将可复用逻辑抽离成函数应该是最直接、成本最低的代码复用方式,但对于状态逻辑,仍然须要经过一些抽象模式(如Observable)才能实现复用,这正是Hooks的思路,将函数做为最小的代码复用单元,同时内置一些模式以简化状态逻辑的复用。比起上面提到的其它方案,Hooks让组件内逻辑复用再也不与组件复用捆绑在一块儿,是真正在从下层去尝试解决(组件间)细粒度逻辑的复用问题此外,这种声明式逻辑复用方案将组件间的显式数据流与组合思想进一步延伸到了组件内。
档案Hooks也并不是完美,只是就目前而言,其缺点以下:

  • 额外的学习成本,主要在于Functional ComponentClass Component之间的比较上。
  • 写法上有限制(不能出如今条件、循环中),而且写法限制增长了重构成本。
  • 破坏了PureComponentReact.memo浅比较的性能优化效果,为了取最新的propsstate,每次render()都要从新建立事件处函数。
  • 在闭包场景可能会引用到旧的stateprops值。
  • 内部实现上不直观,依赖一份可变的全局状态,再也不那么pure
  • React.memo并不能彻底替代shouldComponentUpdate(由于拿不到state change,只针对props change)。
  • useState API设计上不太完美。

示例

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <title>React</title>
</head>

<body>
    <div id="root"></div>
</body>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  const {useState, useEffect} = React;

  function useMouseLocation(location){
    return (
      <>
        <h1>请在此处移动鼠标</h1>
        <p>当前鼠标的位置是: x:{location.x} y:{location.y}</p>
      </>
    );
  }

  function MouseTracker(props){
    const [x, setX] = useState(0);
    const [y, setY] = useState(0);

    function handleMouseMove(event){
        setX(event.clientX);
        setY(event.clientY);
    }
    return (
      <div onMouseMove={handleMouseMove}>
        {useMouseLocation({x, y})}
      </div>
    )
  }

  ReactDOM.render(
    <MouseTracker/>, 
    document.getElementById("root")
  );
</script>

</html>

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://zhuanlan.zhihu.com/p/38136388
https://juejin.cn/post/6844903910470057997
https://juejin.cn/post/6844903850038525959
https://my.oschina.net/u/4663041/blog/4588963
https://zh-hans.reactjs.org/docs/hooks-intro.html
https://zh-hans.reactjs.org/docs/hooks-effect.html
https://react-cn.github.io/react/docs/reusable-components.html
http://www.ayqy.net/blog/react%E7%BB%84%E4%BB%B6%E9%97%B4%E9%80%BB%E8%BE%91%E5%A4%8D%E7%94%A8/
相关文章
相关标签/搜索