从Mixins到HOC再到React Hooks

引言

咱们都知道在业务开发的过程当中,若是彻底不一样的组件有类似的功能,这就会产生横切关注点(cross-cutting concerns)问题。javascript

在React中,存在一些最佳实践去处理横切关注点的问题,能够帮助咱们更好地进行代码的逻辑复用。html

Mixins

针对这个问题,在使用createReactClass建立 React 组件的时候,引入 mixins 功能会是一个很好的解决方案。java

为了在初始阶段更加容易地适应和学习React,官方在 React 中包含了一些急救方案。mixin 系统是其中之一。react

因此咱们能够将通用共享的方法包装成Mixins方法,而后注入各个组件进行逻辑复用的实现。编程

原理

const mixin = function(obj, mixins) {
  const newObj = obj;
  newObj.prototype = Object.create(obj.prototype);
  for (let prop in mixins) {
    if (mixins.hasOwnProperty(prop)) {
      newObj.prototype[prop] = mixins[prop];
    }
  }
  return newObj;
}
复制代码

上述代码就实现了一个简单的mixin函数,其实质就是将mixins中的方法遍历赋值给newObj.prototype,从而实现mixin返回的函数建立的对象都有mixins中的方法,也就是把额外的功能都混入进去。设计模式

在咱们大体明白了mixin做用后,让咱们来看看如何在React使用mixin。数组

应用

var RowMixin = {
  renderHeader: function() {
    return (
      <div className='row-header'> <h1> {this.getHeaderText()} </h1> </div>
    );
  }
};

var UserRow = React.createClass({
  mixins: [RowMixin], // 混入renderHeader方法
  getHeaderText: function() {
    return this.props.user.fullName;
  },
  render: function() {
    return (
      <div> {this.renderHeader()} <h2>{this.props.user.biography}</h2> </div>
    )
  }
}); 
复制代码

使用React.createClass,官方提供了mixins的接入口。须要复用的代码逻辑从这里混入就能够。app

这是ES5的写法,实际上React16版本后就已经废弃了。ide

ES6 自己是不包含任何 mixin 支持。所以,当你在 React 中使用 ES6 class 时,将不支持 mixins 。函数式编程

官方也发现了不少使用 mixins 而后出现了问题的代码库。而且不建议在新代码中使用它们。

缺点

Mixins Considered Harmful

  • Mixins 引入了隐式的依赖关系(Mixins introduce implicit dependencies)

  • Mixins 引发名称冲突(Mixins cause name clashes)

  • Mixins 致使滚雪球式的复杂性(Mixins cause snowballing complexity)

引自官方博客: reactjs.org/blog/2016/0…

官方博客里面有一篇文章详细描述了弃用的缘由。里面列举了三条罪状,如上所述。

在实际开发的过程当中,咱们没法预知别人往代码里mixin了什么属性和状态。若是想要mixin本身的功能,可能会发生冲突,甚至须要去解耦以前的代码。

这样的方式同时也破坏了组件的封装性,代码之间的依赖是不可见的,给重构代码也带来了必定的难度。若是对组件进行修改,极可能会致使mixin方法错误或者失效。

在日后的开发维护过程当中,就致使了滚雪球式的复杂性。

名称冲突

组件中含有多个mixin——

  • 不一样的mixin中含有相同名字的非生命周期函数,React会抛出异常(不是后面的函数覆盖前面的函>数)。

  • 不一样的mixin中含有相同名字的生命周期函数,不会抛出异常,mixin中的相同的生命周期函数(除render方法)会按照createClass中传入的mixins数组顺序依次调用,所有调用结束后再调用组件内部的相同的声明周期函数。

  • 不一样的mixin中默认props或初始state中存在相同的key值时,React会抛出异常。

mixin里面对不一样状况名称冲突的处理,只有当相同名称的生命周期函数,才会按照声明的顺序调用,最后调用组件内部的同名函数。其余状况下都会抛出异常。

mixin这种混入模式,会给组件不断增长新的方法和属性,组件自己不只能够感知,甚至须要作相关的处理(例如命名冲突、状态维护),一旦混入的模块变多时,整个组件就变的难以维护,也就是为何如此多的React库都采用高阶组件的方式进行开发。

HOC

在mixin废弃后,不少开源组件库都是使用的高阶组件写法。

高阶组件属于函数式编程(functional programming)思想。

对于被包裹的组件时不会感知到高阶组件的存在,而高阶组件返回的组件会在原来的组件之上具备功能加强的效果。

高阶函数

说到高阶组件,先要说一下高阶函数的定义。

在数学和计算机科学中,高阶函数是至少知足下列一个条件的函数:

  • 接受一个或多个函数做为输入

  • 输出一个函数

简单地来讲,高阶函数就是接受函数做为输入或者输出的函数。

const add = (x,y,f) => f(x)+f(y);
add(-5, 6, Math.abs);
复制代码

高阶组件

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

高阶组件是一个接受组件而且返回新组件的函数,注意虽然名字叫高阶组件但它自身是一个函数,它能够加强它所包裹的组件功能,或者说赋予了它所包裹的组件一个新的功能。

它不是React API的一部分,源自于React生态,是官方推崇的复用组合的一种方式。它对应着设计模式中的装饰者模式。

高阶组件,主要有两种方式处理包裹组件的方式,分别是属性代理和反向继承。

属性代理(Props Proxy)

实质上是经过包裹原来的组件来操做props

  • 操做props

  • 得到refs引用

  • 抽象state

  • 用其余元素包裹组件

export default function withHeader(WrappedComponent) {
  return class HOC extends Component {
    render() {
      const newProps = {
        test:'hoc'
      }
      // 透传props,而且传递新的newProps
      return <div> <WrappedComponent {...this.props} {...newProps}/> </div> } } } 复制代码

属性代理,其实是经过包裹原来的组件,来注入一些额外的props或者state。

为了加强可维护性,有一些固有的约定,好比命名高阶组件的时候须要使用withSomething的格式。

对于传入的props最好直接透传,不要破坏组件自己的属性和状态。

反向继承(Inheritance Inversion)

  • 渲染劫持

  • 操做props和state

export default function (WrappedComponent) {
  return class Inheritance extends WrappedComponent {
    componentDidMount() {
      // 能够方便地获得state,作一些更深刻的修改。
      console.log(this.state);
    }
    render() {
      return super.render();
    }
  }
}
复制代码

反向继承能够经过super关键字获取到父类原型对象上的全部方法(父类实例上的属性或方法则没法获取)。在这种方式中,它们的关系看上去被反转(inverse)了。

反向继承能够劫持渲染,能够进行延迟渲染/条件渲染等操做。

约定

  • 约定:将不相关的 props 传递给被包裹的组件

  • 约定:包装显示名称以便轻松调试

  • 约定:最大化可组合性

// 而不是这样...
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)
复制代码

compose能够帮助咱们组合任意个(包括0个)高阶函数,例如compose(a,b,c)返回一个新的函数d,函数d依然接受一个函数做为入参,只不过在内部会依次调用c,b,a,从表现层对使用者保持透明。 基于这个特性,咱们即可以很是便捷地为某个组件加强或减弱其特征,只须要去变动compose函数里的参数个数便可。

应用场景

  • 模块复用

  • 页面鉴权

  • 日志及性能打点

例子

export const withTimer = (interval) => (wrappedComponent) => {

  return class extends wrappedComponent {
    constructor(props) {
      super(props);
    }
    // 传入endTime 计算剩余时间戳
    endTimeStamp = DateUtils.parseDate(this.props.endTime).getTime();

    componentWillMount() {
      // 未过时则手动调用计时器 开始倒计时
      if (Date.now() < this.endTimeStamp) {
        this.onTimeChange();
        this.setState({expired: false});
        this.__timer = setInterval(this.onTimeChange, interval);
      }
    }

    componentWillUnmount() {
      // 清理计时器
      clearInterval(this.__timer);
    }

    onTimeChange = () => {
      const now = Date.now();
      // 根据剩余时间戳计算出 时、分、秒注入到目标组件
      const ret = Helper.calc(now, this.endTimeStamp);
      if (ret) {
        this.setState(ret);
      } else {
        clearInterval(this.__timer);
        this.setState({expired: true});
      }
    }

    render() {
      // 反向继承
      return super.render();
    }
  };
};

复制代码
@withTimer()
export class Card extends React.PureComponent {
  render() {
    const {data, endTime} = this.props;
    // 直接取用hoc注入的状态
    const {expired, minute, second} = this.state;
    // 略去render逻辑
    return (...);
  }
}


复制代码

需求是须要进行定时器倒计时,不少组件都须要注入倒计时功能。那么咱们把它提取为一个高阶组件。

这是一个反向继承的方式,能够拿到组件自己的属性和状态,而后把时分秒等状态注入到了组件中。

原组件使用了ES7的装饰器语法,就能够增强它的功能。

组件自己只须要有一个endTime的属性,而后高阶组件就能够计算出时分秒而且进行倒计时。

也就是说,高阶组件赋予了原组件倒计时的功能。

注意

在使用高阶组件写法时,也有一些注意事项。

  • 不要在render函数中使用高阶组件
render() {
  // 每次调用 render 函数都会建立一个新的 EnhancedComponent
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // 这将致使子树每次渲染都会进行卸载,和从新挂载的操做!
  return <EnhancedComponent />; } 复制代码

若是在render函数中建立,每次都会从新渲染一个新的组件。这不只仅是性能问题,每次重置该组件的状态,也可能会引发代码逻辑错误。

  • 静态方法必须复制
// 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 如今使用 HOC
const EnhancedComponent = enhance(WrappedComponent);

// 加强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
复制代码

当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法。

你可使用hoist-non-react-statics自动拷贝全部非 React 静态方法:

  • Refs不会被传递

通常来讲,高阶组件能够传递全部的props属性给包裹的组件,可是不能传递 refs 引用。由于并非像 key 同样,refs 是一个伪属性,React 对它进行了特殊处理。

若是你向一个由高级组件建立的组件的元素添加 ref 应用,那么 ref 指向的是最外层容器组件实例的,而不是包裹组件。

React Hooks

在不编写class的状况下使用state以及其余的React特性。

Hook是一些可让你在函数组件hook react state及生命周期等特性的函数。它不能在class组件中使用。

动机

  • 在组件之间复用状态逻辑

    • render props

      任何被用于告知组件须要渲染什么内容的函数props在技术上均可以被成为称为render prop

      若是在render方法里建立匿名函数,那么使用render prop会抵消使用React.PureComponent带来的优点。 须要把render方法建立为实例函数,或者做为全局变量传入。

    • hoc

    • providers

    • consumers

    这些抽象层组成的组件会造成嵌套地狱,所以React须要为共享状态逻辑提供更好的原生途径。

  • 加强代码可维护性

  • class难以理解

React社区接受了React hooks的提案,这将减小编写 React 应用时须要考虑的概念数量。

Hooks 可使得你始终使用函数,而没必要在函数、类、高阶组件和 reader props之间不断切换。

Hooks

  • 基础 Hook

    • useState

    • useEffect

      启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。

    • useContext

  • 额外的 Hook

    • useReducer

    • useCallback

    • useMemo

    • useRef

    • useImperativeHandle

    • useLayoutEffect

    • useDebugValue

  • 自定义Hook useSomething

    自定义Hook是一种重用状态逻辑的机制,全部的state和反作用都是彻底隔离的。

官方已经弃用了一些生命周期,useEffect至关于componentDidMountcomponentDidUpdatecomponentWillUnmount

除了官方提供的Hook API之外,你可使用自定义Hook。

自定义 Hook 不须要具备特殊的标识。咱们能够自由的决定它的参数是什么,以及它应该返回什么(若是须要的话)。

换句话说,它就像一个正常的函数。可是它的名字应该始终以 use 开头,这样能够一眼看出其符合 Hook 的规则。

动画、订阅声明、计时器是自定义Hook的一些经常使用操做。

接下来,咱们来用React Hook改写一下以前的高阶组件demo。

例子

export function useTimer(endTime, interval, callback) {
  interval = interval || 1000;
  
  // 使用useState Hook get/set状态
  const [expired, setExpired] = useState(true);
  const endTimeStamp = DateUtils.parseDate(endTime).getTime();

  function _onTimeChange () {
    const now = Date.now();
    // 计算时分秒
    const ret = Helper.calc(now, endTimeStamp);
    if (ret) {
      // 回调传出所需的状态
      callback({...ret, expired});
    } else {
      clearInterval(this.__timer);
      setExpired(true);
      callback({expired});
    }
  }

  // 使用useEffect代替生命周期的调用
  useEffect(() => {
    if (Date.now() < endTimeStamp) {
      _onTimeChange();
      setExpired(false);
      this.__timer = setInterval(_onTimeChange, interval);
    }

    return () => {
      // 清除计时器
      clearInterval(this.__timer);
    }
  })
} 
复制代码
export function Card (props) {
  const {data, endTime} = props;
  const [expired, setExpired] = useState(true);
  const [minute, setMinute] = useState(0);
  const [second, setSecond] = useState(0);

  useTimer(endTime, 1000, ({expired, minute, second}) => {
    setExpired(expired);
    setMinute(minute);
    setSecond(second);
  });
  return (...);
复制代码

自定义Hook除了命名须要遵循规则,参数传入和返回结果均可以根据具体状况来定。

这里,我在定时器每秒返回后传出了一个callback,把时分秒等参数传出。

除此以外能够看到没有class的生命周期,使用useEffect来完成反作用的操做。

约定

使用一个eslint-plugin-react-hooksESLint插件来强制执行这些规则

  1. 只在最顶层使用 Hook

不要在循环条件嵌套函数中调用 Hook, 确保老是在React 函数的最顶层调用他们。

由于React是根据你声明的顺序去调用hooks的,若是不在最顶层调用,那么不能保证每次渲染的顺序都是相同的。

遵照规则,React 才可以在屡次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

  1. 只在 React 函数中调用 Hook

    • 在 React 的函数组件中调用 Hook

    • 在自定义 Hook 中调用其余 Hook

参考

React官方文档

Mixins Considered Harmful

深刻浅出React高阶组件

React 高阶组件(HOC)入门指南

Making Sense of React Hooks

相关文章
相关标签/搜索