likes
comments
collection
share

React系列(五)--- 从Mixin到HOC

作者站长头像
站长
· 阅读数 18

系列文章

React系列(一)-- 2013起源 OSCON - React Architecture by vjeux

React系列(二)-- React基本语法实现思路

React系列(三)-- Jsx, 合成事件与Refs

React系列(四)--- virtualdom diff算法实现分析

React系列(五)--- 从Mixin到HOC

React系列(六)--- 从HOC再到HOOKS

Mixins(已废弃)

这是React初期提供的一种组合方案,通过引入一个公用组件,然后可以应用公用组件的一些生命周期操作或者定义方法,达到抽离公用代码提供不同模块使用的目的.

曾经的官方文档demo如下

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

var TickTock = React.createClass({
  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>;
  },
});

React.render(<TickTock />, document.getElementById('example'));

但是Mixins只能应用在createClass的创建方式,在后来的class写法中已经被废弃了.原因在于:

  1. mixin引入了隐式依赖关系
  2. 不同mixins之间可能会有先后顺序甚至代码冲突覆盖的问题
  3. mixin代码会导致滚雪球式的复杂性

详细介绍mixin危害性文章可直接查阅Mixins Considered Harmful

高阶组件(Higher-order component)

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

HOC是一种React的进阶使用方法,大概原理就是接收一个组件然后返回一个新的继承组件,继承方式分两种

属性代理(Props Proxy)

最基本的实现方式

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}

从代码可以看出属性代理方式其实就是接受一个 WrappedComponent 组件作为参数传入,并返回一个继承了 React.Component 组件的类,且在该类的 render() 方法中返回被传入的 WrappedComponent 组件

抽离state & 操作props

在高阶组件控制stateprops再赋值给组件

import React from "react";

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        name: 'PropsProxyHOC',
      };
    }

    logName() {
      console.log(this.name);
    }

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

class Main extends React.Component {
  componentDidMount() {
    this.props.logName();
  }

  render() {
    return <div>PropsProxyHOC</div>;
  }
}

export default PropsProxyHOC(Main);

有种常见的情况是用来做

双向绑定
import React, { Component } from "react";

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = { fields: {} };
    }

    // 深层更新数据
    onChange(fieldName, value) {
      const _s = this.state;
      this.setState({
        fields: {
          ..._s.fields,
          [fieldName]: {
            value: value,
            onChange: _s.fields[fieldName].onChange,
          },
        },
      });
    }

    getField(fieldName) {
      const _s = this.state;
      if (!_s.fields[fieldName]) {
        _s.fields[fieldName] = {
          value: "",
          onChange: (event) => {
            this.onChange(fieldName, event.target.value);
            // 重置输入框
            setTimeout(() => this.onChange(fieldName, ""), 2000);
            // 强行触发render
            this.forceUpdate();
          },
        };
      }

      return {
        value: _s.fields[fieldName].value,
        onChange: _s.fields[fieldName].onChange,
      };
    }

    render() {
      const newProps = {
        fields: this.getField.bind(this),
      };
      // 相当于注入value,onChange属性
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  render() {
    // 相当于设置value,onChange属性
    return <input type="text" {...this.props.fields("name")} />;
  }
}

export default PropsProxyHOC(Main);

获取被继承refs实例

因为这是一个被HOC包装过的新组件,所以想要在HOC里面获取新组件的ref需要用些特殊方式,但是不管哪种,都需要在组件挂载之后才能获取到.并且不能在无状态组件(函数类型组件)上使用 ref 属性,因为无状态组件没有实例。

通过父元素传递方法获取
import React, { Component } from "react";

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    render() {
      const _p = this.props;
      // 动态赋值再注入属性
      const newProps = {};
      // 监听到有对应方法才生成props实例
      typeof _p.getInstance === "function" && (newProps.ref = _p.getInstance);
      return <WrappedComponent {..._p} {...newProps} />;
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  render() {
    return <div>Main</div>;
  }
}

const HOCComponent = PropsProxyHOC(Main);

class ParentComponent extends Component {
  componentWillMount() {
    console.log("componentWillMount: ", this.wrappedInstance); // componentWillMount: undefined;
  }

  componentDidMount() {
    console.log("componentDidMount: ", this.wrappedInstance); // componentDidMount: Main实例
  }

  // 提供给高阶组件调用生成实例
  getInstance(ref) {
    this.wrappedInstance = ref;
  }

  render() {
    return <HOCComponent getInstance={this.getInstance.bind(this)} />;
  }
}

export default ParentComponent;
通过高阶组件当中间层

相比较上一方式,需要在高阶组件提供设置赋值函数,并且需要一个props属性做标记

import React, { Component } from "react";

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    // 暴露给组件的方法,返回ref实例
    getWrappedInstance = () => {
      if (this.props.withRef) {
        return this.wrappedInstance;
      }
    };

    // 暴露给组件的方法,设置ref实例
    setWrappedInstance = (ref) => {
      this.wrappedInstance = ref;
    };

    render() {
      const newProps = {};
      // 监听到有对应方法才赋值props实例
      this.props.withRef && (newProps.ref = this.setWrappedInstance);
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  render() {
    return <div>Main</div>;
  }
}

const HOCComponent = PropsProxyHOC(Main);

class ParentComponent extends Component {
  componentWillMount() {
    console.log("componentWillMount: ", this.refs.child); // componentWillMount: undefined;
  }

  componentDidMount() {
    console.log("componentDidMount: ", this.refs.child.getWrappedInstance()); // componentDidMount: Main实例
  }

  render() {
    return <HOCComponent ref="child" withRef />;
  }
}

export default ParentComponent;
forwardRef(16.3新增)

React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特别有用:

  • 转发 refs 到 DOM 组件
  • 在高阶组件中转发 refs

    const FancyButton = React.forwardRef((props, ref) => (
    <button ref={ref} className="FancyButton">
      {props.children}
    </button>
    ));
    
    // You can now get a ref directly to the DOM button:
    const ref = React.createRef();
    <FancyButton ref={ref}>Click me!</FancyButton>;

    以下是对上述示例发生情况的逐步解释:

  • 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
  • 我们通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>
  • React 传递 ref 给 fowardRef 内函数 (props, ref) => ...,作为其第二个参数。
  • 我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
  • 当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。

劫持渲染

最简单的例子莫过于loading组件了

import React, { Component } from "react";

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    render() {
      // 根据状态渲染界面
      return this.props.isLoading ? (
        <div>Loading...</div>
      ) : (
        <WrappedComponent {...this.props} />
      );
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  render() {
    return <div>Main</div>;
  }
}

const HOCComponent = PropsProxyHOC(Main);

class ParentComponent extends Component {
  constructor() {
    super();
    this.state = {
      isLoading: true,
    };
  }

  render() {
    // 延迟出现主界面
    setTimeout(() => this.setState({ isLoading: false }), 2000);
    return <HOCComponent isLoading={this.state.isLoading} />;
  }
}

export default ParentComponent;

当然也能用于布局上嵌套在其他元素输出

反向继承(Inheritance Inversion)

最简单的demo代码

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

在这里WrappedComponent成了被继承的那一方,从而可以在高阶组件中获取到传递组件的所有相关实例

获取继承组件实例

import React, { Component } from "react";

function InheritanceInversionHOC(WrappedComponent) {
  return class NewComponent extends WrappedComponent {
    componentDidMount() {
      console.log("componentDidMount: ", this); // componentDidMount: NewComponent实例
    }

    render() {
      return super.render();
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  constructor() {
    super();
    this.state = {
      name: "WrappedComponent",
    };
  }

  render() {
    return <div ref="child">Main</div>;
  }
}

export default InheritanceInversionHOC(Main);

cloneElement

再讲解demo之前先科普React的一个方法

React.cloneElement(
  element,
  [props],
  [...children]
)

element 元素为样板克隆并返回新的 React 元素。config 中应包含新的 propskeyref。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。新的子元素将取代现有的子元素,如果在 config 中未出现 keyref,那么原始元素的 keyref 将被保留。React.cloneElement() 几乎等同于:

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

但是,这也保留了组件的 ref。这意味着当通过 ref 获取子节点时,你将不会意外地从你祖先节点上窃取它。相同的 ref 将添加到克隆后的新元素中。

修改props和劫持渲染

相比属性继承来说,反向继承修改props会比较复杂一点

import React, { Component } from "react";

function InheritanceInversionHOC(WrappedComponent) {
  return class NewComponent extends WrappedComponent {
    constructor() {
      super();
      this.state = {
        a: "b",
      };
    }

    render() {
      // 生成实例
      const wrapperTree = super.render();
      // 新的属性
      const newProps = {
        name: "NewComponent",
      };
      // 以 wrapperTree 元素为样板克隆并返回新的 React 元素。
      const newTree = React.cloneElement(
        wrapperTree,
        newProps,
        // 包括组件的子元素也需要保留
        wrapperTree.props.children
      );
      console.log("newTree: ", newTree);
      /* {
        type: "div"
        key: null
        ref: "child"
        props: Object
        children: "Main"
        name: "NewComponent"
        _owner: FiberNode
        _store: Object
      }*/
      return newTree;
    }
  };
}

class Main extends Component {
  render() {
    //原始元素的ref将被保留。
    return <div ref="child">Main</div>;
  }
}

export default InheritanceInversionHOC(Main);
为什么需要用到cloneElement方法?

因为render函数内实际上是调用React.creatElement产生的React元素,尽管我们可以拿到这个方法但是无法修改它.可以用getOwnPropertyDescriptors查看它的配置项,所以用cloneElement创建新的元素替代

相比较属性继承来说,后者只能条件性选择是否渲染WrappedComponent,但是前者可以更加细粒度劫持渲染元素,可以获取到 state,props,组件生命周期(component lifecycle)钩子,以及渲染方法(render),但是依旧不能保证WrappedComponent里的子组件是否渲染,也无法劫持.

注意事项

静态属性失效

// 定义静态函数
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;
}

除了导出组件,另一个可行的方案是再额外导出这个静态方法。

// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;

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

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

渲染机制

React 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。

因为高阶组件返回的是新组件,里面的唯一标志也会变化,所以不建议在render里面也调用高阶组件,这会导致其每次都重新卸载再渲染,即使它可能长得一样.

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

这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有子组件的状态丢失。

如果在组件之外创建 HOC,这样一来组件只会创建一次。因此,每次 render 时都会是同一个组件。一般来说,这跟你的预期表现是一致的。

所以建议高阶组件都是无副作用的纯函数,即相同输入永远都是相同输出,不允许任何有可变因素.

嵌套过深

在原组件中如果包裹层级过多会产生类似回调地狱的烦恼,难以调试,可阅读性糟糕

遵守规则

如果没有规范情况下,也可能造成代码冲突覆盖的局面,例如

  • 将不相关的 props 传递给被包裹的组件
  • 最大化可组合性
  • 包装显示名称以便轻松调试