React开发总结(1):使用高阶组件

什么是mixins

在 React component 构建过程中,常常有这样的场景,有一类功能要被不同的 Component 公用,然后看得到文档经常提到 Mixin(混入)这个术语。Mixin 的特性一直广泛存在于各种面向对象语言。尤其在脚本语言中大都有原生支持,比如 Perl、Ruby、Python,甚至连 Sass 也支持。对于一个前端来说,肯定知道sass,使用sass的mixins,就是为了减少代码量,把可以复用的部分,做成mixins。React 发展初期最主流构建 Component 的方法是利用 createClass 创建:

1
2
3
4
5
6
7
8
9
10
import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';

React.createClass({
mixins: [PureRenderMixin],

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

现在react为什么不推荐使用mixins

React 在发展过程中一直崇尚拥抱标准,尽管它自己看上去是一个异类。当 React 0.13 释出的时候,React 增加并推荐使用 ES6 Classes 来构建 Component。但非常不幸,ES6 Classes 并不原生支持 mixin。尽管 React 文档中也未能给出解决方法,但如此重要的特性没有解决方案,也是一件十分困扰的事。为了增强组件,复用代码,React推荐使用高阶组件来替代mixins,这也是未来发展的趋势.
官方文档截图:
Alt text

什么是高阶组件

High Order Component(高阶组件): 通过函数向现有组件类添加逻辑,就是高阶组件。高阶组件形如: ReactComponent -> ReactComponent
高阶组件是通过闭包和函数,改变已有组件的行为。我们可以通过编写高阶组件,来达到不用修改原有组件的代码就能够增强组件的目的。(高阶组件看起来是不是像lambda表达式呢)

高阶组件在redux中的应用

大家非常熟悉的 react-reduxconnect 函数,就是高阶组件的应用,下面是 connect 的源码结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function connect(mapStateToProps, mapDispatchToProps, 
mergeProps, options = {}) {
//....
return function wrapWithConnect(WrappedComponent) {
//....
class Connect extends Component {
//...
render(
return <WrappedComponent/>
)
}
return Connect

}
}

connect方法接收一个WrappedComponent组件,返回一个Connect组件,有点函数式编程的味道。连接操作不会改变原来的组件类,反而返回一个新的已与 Redux store 连接的组件类,该React组件中注入了state和action creator,达到增强的目的

用ES6编写高阶组件

为了使代码更加优雅,我用es6来编写一个高阶组件:

增强函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 为了能够更好的进行说明,我们将不会修改CartItem组件的代码。
* 而是通过提供一些能够包裹CartItem组件的组件, 并通过一些额
* 外的功能来“增强”CartItem组件。
* 这样的组件我们称之为高阶组件(Higher-Order Component)。
*/
export let IntervalEnhance = ComponsedComponent => class extends Component {
constructor(props) {
super(props);
this.state = {
seconds: this.props.seconds
};
}
componentDidMount() {
this.interval = setInterval(this.tick.bind(this), 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
tick() {
this.setState({
seconds: this.state.seconds + 1
});
}
render() {
/**
* 传递进来的组件,有seconds props,增强函数为其添加了seconds state
* 渲染的时候,重名的属性,后面会覆盖前面的,所以传递进来的属性,就变成了高阶
* 组件的状态,这样在ComponsedComponent里就可以改变seconds了
*/
return <ComponsedComponent {...this.props} {...this.state} />;
}
}

高阶组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {IntervalEnhance, connect} from "./enhance";
/**
* 我们有一个IntervalEnhance函数,我们在CartItem组件中导入它,
* 并通过它来包裹原有的导出对象
*/
class CartItem extends Component {
render() {
return (
<div>
<p>
<strong>Time elapsed for interval: </strong>
{this.props.seconds} s
</p>
</div>
)
}
}
export default IntervalEnhance(CartItem);

在container层中展示CartItem,此时的seconds属性就会随时间递增,并且初始状态为CartItem组件赋予的属性值:

1
2
3
4
5
6
7
8
9
10
render() {

return (
<div>
<CartItem seconds={6}/>
{this.props.children}
</div>

);
}

用ES6把redux的connect函数结构重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export let connect = (mapStateToProps, mapDispatchToProps) => 
WrappedComponent => class extends Component {
state = {
others: {}
}
componentDidMount() {
}
componentWillUnmount() {
}
render() {
const {others} = this.state
const props = {
...this.props,
mapStateToProps,
mapDispatchToProps,
...others
}
return <WrappedComponent {...props} />
}
}

装饰器

此外,ECMAScript的未来标准中还将引入装饰器的概念。Decorator 可以通过返回特定的 descriptor 来”修饰” 类属性,也可以直接”修饰”一个类。即传入一个已有的类,通过 Decorator 函数”修饰”成了一个新的类。那么我们之前的 IntervalEnhance 方法就可以直接被当成 Decorator 来用了。通过这种方法能够更优雅的解决Mixin的问题, 本文不对未标准化的特性做过多的介绍,代码大致如下:

1
2
3
4
5
6
7
import React, {Component} from 'react';
import { IntervalEnhance } from "./interval-enhance";

@IntervalEnhance
export default class CartItem extends Component {
// component code here
}

总结

  1. 通过高阶组件,我们可以不改变原来的组件代码,而给组件加入新的功能
  2. 始终要记住的是,HOC最终返回的是一个新的ReactComponent
  3. mixin是react亲生的,而HOC是社区实践的产物。其实这一点无关紧要,关键是讨论方案是否给开发带来便利
  4. 无论是react-redux 的connect函数,还是redux-form,高阶组件的应用开始随处可见,下次当你想写mixin或class extends的时候,不妨也考虑下高阶组件

源码

本文示例源码

请我吃辣条~~