乐趣区

关于javascript:React系列五-从Mixin到HOC

系列文章

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 传递给被包裹的组件
  • 最大化可组合性
  • 包装显示名称以便轻松调试
退出移动版