系列文章

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() {/*...*/}// 当初应用 HOCconst EnhancedComponent = enhance(WrappedComponent);// 加强组件没有 staticMethodtypeof 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 传递给被包裹的组件
  • 最大化可组合性
  • 包装显示名称以便轻松调试