最近在一些项目中遇到高阶组件的身影,不是很了解,于是深入钻研了一番,以下权当是学习记录了~
Mixin
在谈及高阶组件之前,我们先来讲讲它的前身 mixin ~
mixin 的作用是:如果多个组件中包含相同的方法(包括普通函数和组件生命周期函数),就可以把这一类函数提取到 mixin 中,然后在需要公共方法的组件中使用 mixin, 就可以避免每个组件都去声明一次,从而达到复用。
React 在早期是使用 createClass 来创建一个 Component 的,而且 createClass 支持 mixin 属性,最常见的就是 react-addons-pure-render-mixin 库提供的 PureRenderMixin 方法,用来减少组件使用中一些不必要的渲染,使用方式如下:
import PureRenderMixin from 'react-addons-pure-render-mixin';
React.createClass({
mixins: [PureRenderMixin],
render: function() {
return <div>{this.props.name}</div>;
}
});
和需要在每一个组件中都重复实现一遍 PureRenderMixin 中浅比较的逻辑相比,上面 mixin 中的使用显得更加简便和明了,同时减少了代码的冗余和重复。
minin 既可以定义多个组件中共享的工具方法,同时还可以定义一些组件的生命周期函数(例如上例的 shouldComponentUpdate), 以及初始的 props 和 states。
如下所示:
var propsMixin1 = {
getDefaultProps: () => {
return {
name: "Amy"
};
}
};
var propsMixin2 = {
getDefaultProps: () => {
return {
title: "mixin"
};
}
};
var MixinExample = createReactClass({
mixins: [propsMixin1, propsMixin2],
render: function() {
return (
<div>
<p>{this.props.name}</p>
<p>{this.props.title}</p>
</div>
);
}
});
但是在使用 mixin 的时候,会有如下的几点需要注意:
-
不同 mixin 中有相同的函数
-
-
组件中使用多个 mixin, 同时不同 mixin 中定义了相同的工具函数,此时会报错(而不是前者覆盖后者)
-
-
-
组件中使用多个 mixin, 同时 mixin 中定义了相同的组件生命周期函数,不会报错,此时会按传给 createClass 的 mixin 数组顺序依次调用,全部调用结束后再调用组件内部的相同的生命周期
-
-
不同 mixin 中设置 props 或者 states
-
-
组件中含有多个 mixin,不同的 mixin 中默认 props 或初始 state 中存在相同的 key 值时,React 会抛出异常
-
-
-
组件中含有多个 mixin, 不同的 mixin 中默认 props 或初始 state 中存在不同的 key 值时,则默认 props 和初始 state 都会被合并。
-
附上具体示例代码地址
虽然 mixin 在一定程度上解决了 React 实践中的一些痛点,但是 React 从 v0.13.0 开始,ES6 class 组件写法中不支持 mixins, 但是还是可以使用 createClass 来使用 mixin。之后,React 社区提出了一种新的方式来取代 mixin,那就是高阶组件 Higher-Order Components。
高阶组件
高阶组件 (Higher-Order Components) 是接受一个组件作为参数,然后经过一些处理,返回一个相对增强的组件的函数。它是 React 中的一种模式,而不是 API 的一部分。React 官方给出一个公式描述如下:
const EnhancedComponent = higherOrderComponent(WrappedComponent);
一个最简单的 HOC 例子如下:
function HOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return <WrappedComponent {...this.props}/>
}
}
}
class Example extends React.PureComponent {
render() {
return (
<div>
<p>{this.props.age}</p>
</div>
);
}
}
const HocComponent = HOC(Example);
ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));
高阶组件的适用场景
它的使用场景有如下几点:
-
需要抽离可复用的代码逻辑
-
渲染劫持
-
更改 state
-
组装修改 props
高阶组件的实现方式
高阶组件有两种实现方式: 属性代理 (Props Proxy) 和反向继承 (Inheritance Inversion)
属性代理
属性代理是指所有的数据都是从最外层的 HOC 中传给被包裹的组件,它有权限对传入的数据进行修改,对于被包裹组件来说,HOC 对传给自己的属性 (Props) 起到了一层代理作用。
属性代理可以实现如下一些功能:
-
更改 props
class Example extends React.PureComponent {
constructor(props) {
super(props);
}
render() {
const { name, age, github } = this.props;
return (
<div>
<p>{name}</p>
<p>{age}</p>
<p>{github}</p>
</div>
);
}
}
function HOC(WrappedComponent) {
class EnhancedComponent extends React.PureComponent {
render() {
const props = Object.assign({}, this.props, {
name: "SunShinewyf",
github: "http://github.com/SunShinewyf"
});
return <WrappedComponent {...props} />;
}
}
return EnhancedComponent;
}
const HocComponent = HOC(Example);
ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));
如上面的例子中,HOC 对最外层传入的 props 进行了二次组装,扩展了 props 的数据能力。
通过 refs 获取被包裹的组件实例
class Example extends React.PureComponent {
constructor(props) {
super(props);
this.consoleFun.bind(this);
}
consoleFun() {
console.log("hello world");
}
render() {
const { age } = this.props;
return (
<div>
<p>{age}</p>
</div>
);
}
}
function HOC(WrappedComponent) {
class EnhancedComponent extends React.PureComponent {
initFunc(instance) {
instance.consoleFun();
}
render() {
const props = Object.assign({}, this.props, {
ref: this.initFunc.bind(this)
});
return <WrappedComponent {...props} />;
}
}
return EnhancedComponent;
}
const HocComponent = HOC(Example);
ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));
如果想要在 HOC 中执行被包裹组件的一些方法,就可以在 props 上组装一下 ref 这个属性,就可以获取到被包裹组件的实例,从而获取到实例的 props 以及它的方法。
组装被包裹组件(WrappedComponent)
function HOC(WrappedComponent) {
return class PP extends React.Component {
render() {
<div> //添加一些样式
return <WrappedComponent {...this.props}/>
</div>
}
}
}
这个比较简单,不详述~
反向继承 (Inheritance Inversion)
反向继承是指 HOC 继承被包裹组件,这样被包裹的组件 (WrappedComponent) 就是 HOC 的父组件了,子组件就可以直接操作父组件的所有公开的方法和字段。
反向继承可以实现如下功能:
对 WrappedComponent 的所有生命周期函数进行重写,或者修改其 props 或者 state
class Example extends React.PureComponent {
constructor(props) {
super(props);
}
componentDidMount() {
console.log("wrappedComponent did mount");
}
render() {
const { age } = this.props;
return (
<div>
<p>{age}</p>
</div>
);
}
}
function HOC(WrapperComponent) {
return class Inheritance extends WrapperComponent {
componentDidMount() {
console.log("HOC did mount");
super.componentDidMount();
}
render() {
return super.render();
}
};
}
const HocComponent = HOC(Example);
ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));
// HOC did mount
// wrappedComponent did mount
由上面可以看到,HOC 中定义的生命周期方法可以访问到 WrappedComponent 中的生命周期方法。两者的执行顺序由代码的执行顺序决定。
劫持渲染
class Example extends React.PureComponent {
constructor(props) {
super(props);
}
render() {
const { age } = this.props;
return <input />;
}
}
function HOC(WrapperComponent) {
return class Inheritance extends WrapperComponent {
render() {
const elementsTree = super.render();
let newProps = {};
if (elementsTree && elementsTree.type === "input") {
newProps = { defaultValue: "the initialValue of input" };
}
const props = Object.assign({}, elementsTree.props, newProps);
const newElementsTree = React.cloneElement(
elementsTree,
props,
elementsTree.props.children
);
return newElementsTree;
}
};
}
const HocComponent = HOC(Example);
ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));
运行如上代码,就可以得到一个默认值为 the initialValue of input 的 input 标签。因为 HOC 在 render 之前获取了 WrappedComponent 的 Dom 结构,从而可以自定义一些自己的东西,然后再执行本身的渲染操作。
HOC 的功能虽然很强大,但是在使用过程中还是需要注意,React 官方给出了一些注意事项,在此不赘述~
附上具体示例代码地址
mixin VS HOC
mixin 和 HOC 都能解决代码复用的问题,但是 mixin 存在如下缺点:
-
降低代码的可读性:组件的优势在于将逻辑与是界面直接结合在一起,mixin 本质上会分散逻辑,理解起来难度大
-
mixin 会导致命名冲突:多个 mixin 和组件本身,方法名称会有命名冲突风险,如果遇到了,不得不重命名某些方法
除了上面的显著缺点外,还有一些其他的,详见 Mixins Considered Harmful
而且 HOC 更接近于函数式变成的思想,在使用上也更加灵活,包括的功能点也更多。一张图可以很形象地表达出两者的区别:
由图可以看出 mixin 是一种打补丁的做法,而 HOC 则是组件的嵌套,书写起来也更加优雅~