React组件间逻辑复用

栏目: 服务器 · 发布时间: 4年前

内容简介:React 里,组件是代码复用的主要单元,(摘自很难把状态逻辑拆出来作为一个可复用的函数或组件:

写在前面

React 里,组件是代码复用的主要单元, 基于组合的组件复用机制 相当优雅。而对于更细粒度的逻辑(状态逻辑、行为逻辑等),复用起来却不那么容易:

Components are the primary unit of code reuse in React, but it’s not always obvious how to share the state or behavior that one component encapsulates to other components that need that same state.

(摘自 Use HOCs For Cross-Cutting Concerns

很难把状态逻辑拆出来作为一个可复用的函数或组件:

However, we often can’t break complex components down any further because the logic is stateful and can’t be extracted to a function or another component.

因为 一直以来,都缺少一种简单直接的组件行为扩展方式

React doesn’t offer a way to “attach” reusable behavior to a component (for example, connecting it to a store).

(摘自 It’s hard to reuse stateful logic between components

等等,HOC 不是扩展方式吗,甚至 Mixin 也行啊?

严格来讲,Mixin、Render Props、HOC 等方案都只能算是在既有(组件机制的)游戏规则下探索出来的上层模式:

To be clear, mixins is an escape hatch to work around reusability limitations in the system. It’s not idiomatic React.

(摘自 Proposal for porting React’s Mixin APIs to a generic primitive

HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.

(摘自 Higher-Order Components

一直没有从根源上很好地解决组件间逻辑复用的问题……直到 Hooks 登上舞台

P.S.Mixin 看似属于下层解决方案(React 提供了内部支持),实际上只是内置了一个 mixin() 工具函数,唯一特殊之处是冲突处理策略:

A class can use multiple mixins, but no two mixins can define the same method. Two mixins can, however, implement the same lifecycle method. In this case, each implementation will be invoked one after another.

一.探索

为了进一步复用组件级以下的细粒度逻辑(比如处理横切关注点),探索出了种种方案:

大致过程是这样:

理论基础 方案 缺陷
照搬 借鉴OOP复用模式 Mixin 组件复杂度陡升,难以理解
声明式优于命令式,组合优于继承 Higher-Order Components, Render Props 多重抽象导致Wrapper Hell
借鉴函数式思想 Hooks 写法限制、学习成本等

二.Mixin

Mixins allow code to be shared between multiple React components. They are pretty similar to mixins in Python or traits in PHP.

Mixin 方案的出现源自一种 OOP 直觉,虽然 React 本身有些函数式味道,但为了迎合用户习惯,早期只提供了 React.createClass() API 来定义组件:

React is influenced by functional programming but it came into the field that was dominated by object-oriented libraries. It was hard for engineers both inside and outside of Facebook to give up on the patterns they were used to.

自然而然地,(类)继承就成了一种直觉性的尝试。而在 JavaScript 基于原型的扩展模式下,类似于继承的 Mixin 方案就成了首选:

// 定义Mixin
var Mixin1 = {
  getMessage: function() {
    return 'hello world';
  }
};
var Mixin2 = {
  componentDidMount: function() {
    console.log('Mixin2.componentDidMount()');
  }
};

// 用Mixin来增强现有组件
var MyComponent = React.createClass({
  mixins: [Mixin1, Mixin2],
  render: function() {
    return <div>{this.getMessage()}</div>;
  }
});

(摘自上古文档 react/docs/docs/mixins.md

Mixin 主要用来解决生命周期逻辑和状态逻辑的复用问题:

It tries to be smart and “merges” lifecycle hooks. If both the component and the several mixins it uses define the componentDidMount lifecycle hook, React will intelligently merge them so that each method will be called. Similarly, several mixins can contribute to the getInitialState result.

允许从外部扩展组件生命周期,在Flux等模式中尤为重要:

It’s absolutely necessary that any component extension mechanism has the access to the component’s lifecycle.

缺陷

但存在诸多缺陷:

  • 组件与 Mixin 之间存在隐式依赖(Mixin 经常依赖组件的特定方法,但在定义组件时并不知道这种依赖关系)

  • 多个 Mixin 之间可能产生冲突(比如定义了相同的 state 字段)

  • Mixin 倾向于增加更多状态,这降低了应用的可预测性(The more state in your application, the harder it is to reason about it.),导致复杂度剧增

隐式依赖导致依赖关系不透明,维护成本和理解成本迅速攀升:

  • 难以快速理解组件行为,需要全盘了解所有依赖 Mixin 的扩展行为,及其之间的相互影响

  • 组价自身的方法和 state 字段不敢轻易删改,因为难以确定有没有 Mixin 依赖它

  • Mixin 也难以维护,因为 Mixin 逻辑最后会被打平合并到一起,很难搞清楚一个 Mixin 的输入输出

毫无疑问, 这些问题是致命的

所以, React v0.13.0 放弃了 Mixin (继承),转而走向 HOC (组合):

Idiomatic React reusable code should primarily be implemented in terms of composition and not inheritance.

示例

(不考虑 Mixin 方案存在的问题)单从功能上看,Mixin 同样能够完成类似于 HOC 的扩展,例如:

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

// 等价于React v15.5.0以下的React.createClass
var createReactClass = require('create-react-class');

var TickTock = createReactClass({
  mixins: [SetIntervalMixin], // Use the mixin
  getInitialState: function() {
    return {seconds: 0};
  },
  componentDidMount: function() {
    this.setInterval(this.tick, 1000); // Call a method on the 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')
);

(摘自 Mixins

P.S.[React v15.5.0]正式废弃 React.createClass() API,移至 create-react-class ,内置 Mixin 也一同成为历史,具体见 React v15.5.0

三.Higher-Order Components

Mixin 之后,HOC 担起重任,成为组件间逻辑复用的推荐方案:

A higher-order component (HOC) is an advanced technique in React for reusing component logic.

HOC 并不是新秀 ,早在 React.createClass() 时代就已经存在了,因为 HOC 建立在组件组合机制之上,是完完全全的上层模式,不依赖特殊支持

形式上类似于高阶函数,通过包一层组件来扩展行为:

Concretely, A higher-order component is a function that takes a component and returns a new component.

例如:

// 定义高阶组件
var Enhance = ComposedComponent => class extends Component {
  constructor() {
    this.state = { data: null };
  }
  componentDidMount() {
    this.setState({ data: 'Hello' });
  }
  render() {
    return <ComposedComponent {...this.props} data={this.state.data} />;
  }
};

class MyComponent {
  render() {
    if (!this.data) return <div>Waiting...</div>;
    return <div>{this.data}</div>;
  }
}
// 用高阶组件来增强普通组件,进而实现逻辑复用
export default Enhance(MyComponent);

理论上,只要接受组件类型参数并返回一个组件的函数都是高阶组件( (Component, ...args) => Component ),但为了方便组合,推荐 Component => Component 形式的 HOC,通过偏函数应用来传入其它参数,例如:

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

对比 Mixin

HOC 模式下,外层组件通过 Props 影响内层组件的状态,而不是直接改变其 State:

Instead of managing the component’s internal state, it wraps the component and passes some additional props to it.

并且,对于可复用的状态逻辑,这份状态只维护在带状态的高阶组件中(相当于扩展 State 也有了组件作用域),不存在冲突和互相干扰的问题:

// This function takes a component...
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} />;
    }
  };
}

最重要的,不同于 Mixin 的 打平+合并HOC 具有天然的层级结构(组件树结构),这种分解大大降低了复杂度

This way wrapper’s lifecycle hooks work without any special merging behavior, by the virtue of simple component nesting!

缺陷

HOC 虽然没有那么多致命问题,但也存在一些小缺陷:

  • 扩展性限制:HOC 并不能完全替代 Mixin

  • Ref 传递问题:Ref 被隔断

  • Wrapper Hell:HOC 泛滥,出现 Wrapper Hell

扩展能力限制

一些场景下,Mixin 可以而 HOC 做不到,比如 PureRenderMixin

PureRenderMixin implements shouldComponentUpdate, in which it compares the current props and state with the next ones and returns false if the equalities pass.

因为 HOC 无法从外部访问子组件的 State,同时通过 shouldComponentUpdate 滤掉不必要的更新。因此,React 在支持 ES6 Class 之后提供了 React.PureComponent 来解决这个问题

Ref 传递问题

Ref 的传递问题在层层包装下相当恼人,函数 Ref 能够缓解一部分(让 HOC 得以获知节点创建与销毁),以致于后来有了 React.forwardRef API:

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;

      // Assign the custom prop "forwardedRef" as a ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // Note the second param "ref" provided by React.forwardRef.
  // We can pass it along to LogProps as a regular prop, e.g. "forwardedRef"
  // And it can then be attached to the Component.
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

(摘自 Forwarding refs in higher-order components

Wrapper Hell

没有包一层解决不了的问题,如果有,那就包两层……

Wrapper Hell 问题紧随而至:

You will likely find a “wrapper hell” of components surrounded by layers of providers, consumers, higher-order components, render props, and other abstractions.

多层抽象同样增加了复杂度和理解成本 ,这是最关键的缺陷,而 HOC 模式下没有很好的解决办法

四.Render Props

与 HOC 一样,Render Props 也是一直以来都存在的元老级模式:

The term “render prop” refers to a technique for sharing code between React components using a prop whose value is a function.

例如抽离复用光标位置相关渲染逻辑,并通过 Render Props 模式将可复用组件与目标组件组合起来:

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

组件的一部分渲染逻辑由外部通过 Props 提供,其余不变的部分可以复用

类比 HOC

技术上,二者都基于组件组合机制,Render Props 拥有与 HOC 一样的扩展能力

称之为 Render Props,并不是说只能用来复用渲染逻辑:

In fact, any prop that is a function that a component uses to know what to render is technically a “render prop”.

(摘自 Using Props Other Than render

而是表示在这种模式下,组件是通过 render() 组合起来的,类似于 HOC 模式下通过 Wrapper 的 render() 建立组合关系

形式上,二者非常相像,同样都会产生一层“Wrapper”( EComponentRP ):

// HOC定义
const HOC = Component => WrappedComponent;
// HOC使用
const Component;
const EComponent = HOC(Component);
<EComponent />

// Render Props定义
const RP = ComponentWithSpecialProps;
// Render Props使用
const Component;
<RP specialRender={() => <Component />} />

更有意思的是, Render Props 与 HOC 甚至能够相互转换

function RP2HOC(RP) {
  return Component => {
    return class extends React.Component {
      static displayName = "RP2HOC";
      render() {
        return (
          <RP
            specialRender={renderOptions => (
              <Component {...this.props} renderOptions={renderOptions} />
            )}
          />
        );
      }
    };
  };
}
// 用法
const HOC = RP2HOC(RP);
const EComponent = HOC(Component);

function HOC2RP(HOC) {
  const RP = class extends React.Component {
    static displayName = "HOC2RP";
    render() {
      return this.props.specialRender();
    }
  };
  return HOC(RP);
}
// 用法
const RP = HOC2RP(HOC);
<RP specialRender={() => <Component />} />

在线 Demo: https://codesandbox.io/embed/hocandrenderprops-0v72k

P.S.视图内容完全一样,但组件树结构差别很大:

React组件间逻辑复用

react hoc to render props

可以通过 React DevTools 查看 https://0v72k.codesandbox.io/

五.Hooks

HOC、Render Props、组件组合、Ref 传递…… 代码复用为什么这样复杂?

根本原因在于 细粒度代码复用不应该与组件复用捆绑在一起

Components are more powerful, but they have to render some UI. This makes them inconvenient for sharing non-visual logic. This is how we end up with complex patterns like render props and higher-order components.

HOC、Render Props 等基于组件组合的方案,相当于先把要复用的逻辑包装成组件,再利用组件复用机制实现逻辑复用。自然就受限于组件复用,因而出现扩展能力受限、Ref 隔断、Wrapper Hell……等问题

那么,有没有一种简单直接的代码复用方式?

函数。将可复用逻辑抽离成函数应该是最直接、成本最低的代码复用方式:

Functions seem to be a perfect mechanism for code reuse. Moving logic between functions takes the least amount of effort.

但对于状态逻辑,仍然需要通过一些抽象模式(如 Observable )才能实现复用:

However, functions can’t have local React state inside them. You can’t extract behavior like “watch window size and update the state” or “animate a value over time” from a class component without restructuring your code or introducing an abstraction like Observables.

这正是 Hooks 的思路:将函数作为最小的代码复用单元,同时内置一些模式以简化状态逻辑的复用

例如:

function MyResponsiveComponent() {
  const width = useWindowWidth(); // Our custom Hook
  return (
    <p>Window width is {width}</p>
  );
}

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  });

  return width;
}

(摘自 Making Sense of React Hooks ,在线 Demo 见 https://codesandbox.io/embed/reac-conf-2018-dan-abramov-hooks-example-mess-around-o5zcu

声明式状态逻辑( const width = useWindowWidth() ),语义非常自然

对比其它方案

比起上面提到的其它方案,Hooks 让组件内逻辑复用不再与组件复用捆绑在一起,是真正在从下层去尝试解决(组件间)细粒度逻辑的复用问题

此外,这种声明式逻辑复用方案 将组件间的显式数据流与组合思想进一步延伸到了组件内 ,契合 React 理念:

Hooks apply the React philosophy (explicit data flow and composition) inside a component, rather than just between the components.

缺陷

Hooks 也并非完美,只是就目前而言,其缺点如下:

  • 额外的学习成本(Functional Component 与 Class Component 之间的困惑)

  • 写法上有限制(不能出现在条件、循环中),并且写法限制增加了重构成本

  • 破坏了 PureComponentReact.memo 浅比较的性能优化效果(为了取最新的 propsstate ,每次 render() 都要重新创建事件处函数)

  • 在闭包场景可能会引用到旧的 stateprops

  • 内部实现上不直观(依赖一份可变的全局状态,不再那么“纯”)

  • React.memo 并不能完全替代 shouldComponentUpdate (因为拿不到 state change,只针对 props change)

  • useState API 设计上不太完美

(摘自 Drawbacks

参考资料


以上所述就是小编给大家介绍的《React组件间逻辑复用》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

流量池

流量池

杨飞 / 中信出版集团 / 2018-4 / 68.00

移动互联网时代,信息日益冗余,新闻速朽; 整体流量增长速度放缓,而竞争者数量高速增加; 流量呈现变少、变贵、欺诈频繁的现状; 品效合一的营销策略成为共识,而实现路径成为痛点; 多次开创各营销渠道效果之最的营销人、各种刷屏级营销事件操盘手、神州专车CMO杨飞,这一次倾囊相授,诚恳讲述如何实现流量获取、营销转化以及流量的运营和再挖掘。一起来看看 《流量池》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具