React Redux - 容器型组件的作用与其生成方式

本文总结了展示型组件和容器型组件的差别,解释了为什么需要 React Redux 的connect()及其工作原理。

一、展示型组件与容器型组件

1.1 展示型组件

  1. 关注于“组件展示的外在形态是什么样的”;

  2. 内部可能会包含展示型组件和容器型组件,通常会有一些DOM标签和样式;

  1. 不依赖于应用的其余部分,如 Flux 的 actionstore ;

  2. 很少有自己的state,如果有,则是 UI state, 而不是数据;

  3. 一般可以用function手动编写,除非需要state, 生命周期钩子,性能优化等就用class。

1.2 容器型组件

  1. 关注于“组件是如何运转的”;

  2. 内部可能包含展示型和容器型组件,但是一般不会有DOM标签(除了用于包裹的div标签)和样式;

  3. 为展示型组件或其他容器型组件提供数据和行为;

  4. 调用Flux actions,并以回调的形式提供给展示型组件;

  5. 通常是有状态的,通常作为数据来源;

  6. 一般通过高阶组件来产生,例如:React Redux 的connect(), Relay 的 createContainer(), Flux Utils 的Container.create(), 而不是手动编写。(参见本文第六部分)

1.3 表格总结

展示型组件 容器型组件
功能 决定程序如何显示(模板,样式) 决定程序如何运作(数据获取,状态更新)
是否连接 Redux
读取数据 props中读取 订阅 Redux 的state
更新数据 调用来自props的回调函数 发起 Redux 的 action
生成方式 手动编写 大多通过 react-redux 自动生成

1.4 将组件分为这两类的好处

  1. 关注点分离。这样编写组件可以让你更好地理解你的应用和UI;

  2. 更好的复用性。你可以使用同一个展示型组件,给它完全不同的数据来源,将这些封装成独立的容器型组件,可进行更广泛地复用;

  3. 展示型组件实质上是你的应用的“调色板”。你可以将其放在一个单页面上,设计师就可以调整它的样式,而不用接触到应用的逻辑。你也可以在这个页面上运行截图回归测试。

  4. 强制你将布局组件(如,Sidebar, Page, ContextMenu)抽离出来,在其他容器型组件中使用this.props.children引入,而不是复制同样的标签和布局。

  5. 补充说明:

将组件分为这两类是React与Redux开发的最佳实践,但这个约定并非需要死板地遵循:

containers目录中也可能放置没有连接Redux的组件,因为有些页面就是不需要连接Redux但它仍然是其他组件的容器;

components目录中也可以放置连接Redux的组件,比如 ReduxForm 生成的表单组件。

但绝大多数情况下,都应将 Redux 连接在组件顶层,不让里面的组件感受到 Redux 的存在。

二、什么时候需要容器型组件?

如果你需要从上而下传递很多的props,而传递过程中,有些中间组件并不需要使用这些props,仅仅是把它们继续往下传递(因为props只能逐级传递),每当你的目标子组件需要新数据时你就得再次经过这些中间组件,这时需要引入容器型组件来解决这个问题。

容器型组件生成前后对比图

容器型组件会将 Redux store 中的数据连接到展示组件。(参见本文第三部分)

三、如何生成容器型组件

3.1 Redux基本用法

通过 reducer 创建一个 store,每当我们在 store 上 dispatch 一个 action,store 内的数据就会相应地发生变化。

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
34
35
36
37
38
39
40
41
42
43
44
const reducer = (state = {count: 0}, action) => {
switch (action.type){
case 'INCREASE': return {count: state.count + 1};
case 'DECREASE': return {count: state.count - 1};
default: return state;
}
}
const actions = {
increase: () => ({type: 'INCREASE'}),
decrease: () => ({type: 'DECREASE'})
}
const store = createStore(reducer);
store.subscribe(() =>
console.log(store.getState())
);
store.dispatch(actions.increase()) // {count: 1}
store.dispatch(actions.increase()) // {count: 2}
store.dispatch(actions.decrease()) // {count: 1}
```
### 3.2 方法一:手动生成
使用 `store.subscribe()` 从 Redux state 树中读取部分数据,并通过 `props` 来把这些数据提供给要渲染的组件。
```javascript
class App extends Component{
componentWillMount(){
//使用store.subscribe监听state变化
store.subscribe((state)=>this.setState(state))
}
render(){
//将 state 上的属性作为 props 层层传递下去
return <Comp state={this.state}
onIncrease={()=>store.dispatch(actions.increase())}
onDecrease={()=>store.dispatch(actions.decrease())}
/>
}
}

这种方法的不好之处在于:

  • 无法直接给子组件传递state和方法;

  • 任意state的变化都会导致整个组件树的重新渲染。(可能需要为了性能优化而手动实现 React 性能优化建议 中的 shouldComponentUpdate 方法。)

3.3 方法二:使用 react-redux 连接

3.3.1 用法

主要是 Providerconnect的使用。

将所有内容包裹在Provider中,并将store作为prop传递给Provider

1
2
3
4
5
6
7
const App = () => {
return (
<Provider store={store}>
<Comp/>
</Provider>
)
};

Provider中的任何组件(比如Comp),如果要使用state中的数据,要求Comp是被connect方法包装过的组件(容器型组件):

1
2
3
4
5
6
class MyComp extends Component {
// content...
}
//使用connect生成容器型组件,后面会介绍connect的参数
const Comp = connect(...args)(MyComp);

注意:推荐使用react-redux而不是手动编写来生成容器型组件。因为connect()方法做了性能优化来避免很多不必要的重复渲染。

3.3.2 connect的参数

connect(mapStateToProps, mapDispatchToProps, mergeProps, options)

1) mapStateToProps函数

(state, ownProps) => stateProps

将store中的数据作为props绑定到组件上:

  • 第一个参数state是 Redux 的store
1
2
3
4
5
6
const mapStateToProps = (state) => {
return {
count: state.count, //直接传递store中的数据
greaterThanFive: state.count > 5 //将store中的数据处理后传递
}
}

绑定后,组件MyComp可以通过this.props访问到store中的数据:

1
2
3
4
5
6
7
8
9
10
class MyComp extends Component {
render(){
return <div>
计数:{this.props.count}次,
是否大于5:{this,props.greaterThanFive}
</div>
}
}
const Comp = connect(...args)(MyComp);
  • 第二个参数ownProps是原组件MyComp自身的props。有时会用到,比如,当你在 store中维护了一个用户列表,而你的组件MyComp只关心一个用户(通过props中的 `userId体现):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const mapStateToProps = (state, ownProps) => {
// state 是 {userList: [{id: 0, name: '王二'}]}
return {
user: _.find(state.userList, {id: ownProps.userId})
}
}
class MyComp extends Component {
static PropTypes = {
userId: PropTypes.string.isRequired,
user: PropTypes.object
};
render(){
return <div>用户名:{this.props.user.name}</div>
}
}
const Comp = connect(mapStateToProps)(MyComp);

state或者ownProps变化的时候,mapStateToProps都会被调用,计算出一个新的 stateProps,(在与ownProps merge 后)更新给MyComp

2) mapDispatchToProps

(dispatch, ownProps) => dispatchProps

action作为props绑定到MyComp上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const mapDispatchToProps = (dispatch, ownProps) => {
return {
increase: () => dispatch(actions.increase()),
decrease: () => dispatch(actions.decrease())
}
}
class MyComp extends Component {
render(){
const {count, increase, decrease} = this.props;
return (<div>
<div>计数:{this.props.count}次</div>
<button onClick={increase}>增加</button>
<button onClick={decrease}>减少</button>
</div>)
}
}
const Comp = connect(mapStateToProps, mapDispatchToProps)(MyComp);

Q:为什么不直接将action对象传递给MyComp组件,而要传递函数increasedecrease,包装dispatch分发该action的过程?

A:为了不让MyComp组件感知到dispatch的存在,将action包装成可以直接被调用的函数。Redux的bindActionCreators函数也可以做到:

1
2
3
4
5
6
7
8
import {bindActionCreators} from 'redux';
const mapDispatchToProps = (dispatch, ownProps) => {
return bindActionCreators({
increase: action.increase,
decrease: action.decrease
});
}

函数bindActionCreators(actionCreators, dispatch)会返回一个对象,该对象中的每个函数都可以直接dipatch相应的action

3) mergeProps

(stateProps, dispatchProps, ownProps) => props

将前面两个函数生成的statePropsdispatchProps,与原组件MyComp原有属性ownProps合并后赋给MyComp.通常情况下,如果不传这个参数,connect会默认使用Object.assign()替代这个方法。

四、Provider和connect的工作原理

4.1 Provider

Provider是个 React 组件,它通过context而不是propsstore传递给子组件,所以可以跨级传递。下面是它的部分源代码:

node_modules/react-redux/src/components/Provider.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default class Provider extends Component {
getChildContext() {
return { store: this.store }
}
...
}
...
Provider.propTypes = {
store: storeShape.isRequired,
...
}
Provider.childContextTypes = {
store: storeShape.isRequired
}

4.2 connect

Connect是一个嵌套函数,运行connect()后,会生成一个高阶组件,该高阶组件接收一个组件作为参数再次运行,会生成一个新组件(connect组件)。

connect组件从context中获取来自Providerstore,然后从store获取statedispatch,最后将state和经过dispatch加工过的action创建函数连接到组件上,并在state变化时重新渲染组件。

react-redux所做的性能优化:如果一个页面有多个被connect()连接的组件,这些组件只会在自己对应的state变化时重新渲染(重新渲染前会检测传入组件的数据是否变化,如果没变化就不会执行旋绕),所以不组件的数据如果用connect()隔离开,它们的渲染不会相互干扰。

如下简单列出它的部分源码(主要是看出高阶组件):

node_modules/react-redux/src/components/connect.js

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
34
35
36
37
38
39
40
41
42
43
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
...
return function wrapWithConnect(WrappedComponent) {
...
class Connect extends Component {
shouldComponentUpdate() {
return !pure || this.haveOwnPropsChanged || this.hasStoreStateChanged
}
constructor(props, context) {
super(props, context)
...
this.store = props.store || context.store
...
}
...
render() {
...
return this.renderedElement
}
}
...
Connect.WrappedComponent = WrappedComponent
...
return hoistStatics(Connect, WrappedComponent)
}
}

五、补充:一些对立词汇的解释

1 “有状态(stateful)”与“无状态(stateless)”

某些组件会使用React的setState()方法,而有的组件不用。

容器型组件倾向于是“有状态”的,展示型组件倾向于是“无状态”的,但这也不是绝对的。

2 class 与 function

二者都可以用于定义组件。

用函数定义组件更简单和易于理解,但是缺少某些只对class可用的特性,比如state, 声明周期钩子,性能优化。将来有可能会减少这些限制。

3 “纯净(pure)”与“非纯净(impure)”

  • “纯净的”组件:对于相同的propsstate, 总是输出同样的结果。

  • 组件纯净与否与其定义方式和有无状态无关。

  • 纯组件不依赖于其propsstate的变化,因此它们的渲染性能可以在shouldComponentUpdate()钩子函数中浅对比(shallow comparison)来优化。

这里简要介绍一下浅对比

ES6类使用React时,使用shallowCompare完成和PureRenderMixin相同的功能:如果组件的绘制函数是“纯净的”,可以使用这个辅助函数在某些情况下提升性能。

propsnextProps对象,statenextState对象的键值key分别进行迭代比较,在key值!==时,shouldComponentUpdate()函数就返回true,说明组件应该更新。

参考资料

  1. Redux官方文档 - 中文版

  2. Presentational and Container Components - Medium

  3. React 实践心得:react-redux 之 connect 方法详解

  4. shallowCompare

  5. React + Redux 入坑指南

  6. 《React与Redux开发实例精解》,刘一奇著,电子工业出版社,2016.11

分享
0%