https://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html
UI组件
UI 组件有以下几个特征。
- 只负责 UI 的呈现,不带有任何业务逻辑
- 没有状态(即不使用
this.state
这个变量) - 所有数据都由参数(
this.props
)提供 - 不使用任何 Redux 的 API
1 | const Title = |
因为不含有状态,UI 组件又称为”纯组件”,即它纯函数一样,纯粹由参数决定它的值。
容器组件
容器组件的特征
- 负责管理数据和业务逻辑,不负责 UI 的呈现
- 带有内部状态
- 使用 Redux 的 API
总之,只要记住一句话就可以了:UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。
如果一个组件既有 UI 又有业务逻辑,那怎么办?
- 将它拆分成下面的结构:外面是一个容器组件,里面包了一个UI 组件。
- 前者负责与外部的通信,将数据传给后者,由后者渲染出视图。
React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。
connect()
- React-Redux 提供
connect
方法,用于从 UI 组件生成容器组件。 connect
的意思,就是将这两种组件连起来。
1 | import { connect } from 'react-redux' |
上面代码中,
TodoList
是 UI 组件,VisibleTodoList
就是由 React-Redux 通过connect
方法自动生成的容器组件。
但是,因为没有定义业务逻辑,上面这个容器组件毫无意义,只是 UI 组件的一个单纯的包装层。为了定义业务逻辑,需要给出下面两方面的信息。
- 输入逻辑:外部的数据(即
state
对象)如何转换为 UI 组件的参数 - 输出逻辑:用户发出的动作如何变为 Action 对象,从 UI 组件传出去。
因此,connect
方法的完整 API 如下。
1 | import { connect } from 'react-redux' |
- 上面代码中,
connect
方法接受两个参数:mapStateToProps
和mapDispatchToProps
。 - 它们定义了 UI 组件的业务逻辑。
- 前者负责输入逻辑,即将
state
映射到 UI 组件的参数(props
), - 后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。
- 前者负责输入逻辑,即将
mapStateToProps()
mapStateToProps
是一个函数。- 它的作用就是像它的名字那样,建立一个从(外部的)
state
对象到(UI 组件的)props
对象的映射关系。(建立一个从redux的state对象到UI组件的Props对象的映射关系) - 作为函数,
mapStateToProps
执行后应该返回一个对象,里面的每一个键值对就是一个映射。
说人话:mapStateToProps这个函数允许我们将 redux中的store 中的数据作为 props 绑定到UI组件上。
1 | const mapStateToProps = (state) => { |
上面代码中,mapStateToProps
是一个函数,它接受state
作为参数,返回一个对象。这个对象有一个todos
属性,代表 UI 组件的同名参数,后面的getVisibleTodos
也是一个函数,可以从state
算出 todos
的值。
mapStateToProps
会订阅 Store,每当state
更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。mapStateToProps
的第一个参数总是state
对象,还可以使用第二个参数,代表容器组件的props
对象。
1 | // 容器组件的代码 |
使用ownProps
作为参数后,如果容器组件的参数发生变化,也会引发 UI 组件重新渲染。
connect
方法可以省略mapStateToProps
参数,那样的话,UI 组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。
mapDispatchToProps()
mapDispatchToProps
是connect
函数的第二个参数,用来建立 UI 组件的参数到store.dispatch
方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。
说人话:mapDispatchToProps,它的功能是,将 action 作为 props 绑定到 MyComp 上。
我们可以方便得使用去调用
1 <div onCLick={()=>this.props.del_todo() }>test</div>
如果mapDispatchToProps
是一个函数,会得到dispatch
和ownProps
(容器组件的props
对象)两个参数
1 | const mapDispatchToProps = ( |
从上面代码可以看到,mapDispatchToProps
作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。
如果mapDispatchToProps
是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。举例来说,上面的mapDispatchToProps
写成对象就是下面这样。
1 | const mapDispatchToProps = { |
总结
UI 组件负责 UI 的呈现,容器组件负责
管理数据
和逻辑
。所以,UI组件就需要两种参数,
- 数据:用于层现内容
- 逻辑:这里是function,也就是onClick监听事件触发的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const Todo = ({ onClick, completed, text }) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
)
// todos用于数据层现,toggleTodo就是onClick监听事件触发的函数
const TodoList = ({ todos, toggleTodo }) => (
<ul>
{todos.map(todo => (
<Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
))}
</ul>
)如果一个组件既有 UI 又有业务逻辑,那怎么办?回答是,将它拆分成下面的结构:外面是一个容器组件,里面包了一个UI 组件。前者负责与外部的通信,将数据传给后者,由后者渲染出视图。
所以一般都是容器组件的里面包着一个UI组件。同时容器组件又是需要react-redux自动生成的。所以需要容器组件传递给UI组件这两种参数(数据和逻辑)
所以需要通过
mapStateToProps
传递数据,通过mapDispatchToProps
传递逻辑。其具体功能是:
- 当state发生改变时(比如{filter:”show_all”}变成{filter:”show_complete”}),会自动执行
mapStateToProps
,该函数会让容器组件自动产生新的prop(也就是{filter:"show_complete"}
),并将props传给UI组件,这样就触发了UI组件的重新渲染。 - 当容器组件监听到Onclick事件时,就会自动触发
mapDispatchToProps
函数,返回Onclick对应的处理函数,并且将这个处理函数传递给UI组件,让UI组件发送Action。
- 当state发生改变时(比如{filter:”show_all”}变成{filter:”show_complete”}),会自动执行
具体的方式就是返回一个
映射
- TodoList组件需要
数据型参数
todos,所以返回属性todos的映射 - TodoList组件需要
逻辑型参数
toggleTodo,所以返回属性toggleTodo的映射
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// todos用于数据层现,toggleTodo就是onClick监听事件触发的函数
const TodoList = ({ todos, toggleTodo }) => (
<ul>
{todos.map(todo => (
<Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
))}
</ul>
)
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
case 'SHOW_ALL':
default:
return todos
}
}
const mapStateToProps = state => ({
todos: getVisibleTodos(state.todos, state.visibilityFilter)
})
const mapDispatchToProps = dispatch => ({
toggleTodo: id => dispatch(toggleTodo(id))
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)- TodoList组件需要
场景化描述:
- 用户点击了UI组件
- UI组件监听了OnClick事件,但是自己没有相关的处理函数,便利用
mapDispatchToProps
去容器组件里找 - 在容器组件找到对应的处理函数后,调用他,该函数会发出一个action
- 该action被reduers捕捉,处理,返回新的state
- 返回新的state被容器组件接收,自动调用
mapStateToProps
返回新的prop,然后将新的prop传给UI组件 - UI组件得到新的Prop后就可以重新渲染
mapStateToProps(state, ownProps)
mapStateToProps
是一个函数,用于建立组件跟store
的state
的映射关系- mapStateToProps这个函数允许我们将 store 中的数据作为 props 绑定到组件上。
- 作为一个函数,它可以传入两个参数,结果一定要返回一个
object
- 传入
mapStateToProps
之后,会订阅store
的状态改变,在每次store
的state
发生变化的时候,都会被调用 ownProps
代表组件本身的props,如果写了第二个参数ownProps
,那么当prop
发生变化的时候,mapStateToProps
也会被调用。例如,当props
接收到来自父组件一个小小的改动,那么你所使用的ownProps
参数,mapStateToProps
都会被重新计算)。mapStateToProps
可以不传,如果不传,组件不会监听store的变化,也就是说Store的更新不会引起UI的更新
mapDispatchToProps
mapDispatchToProps
用于建立组件跟store.dispatch
的映射关系- mapDispatchToProps的功能是,将 action 作为 props 绑定到 MyComp 上
- 可以是一个
object
,也可以传入函数 - 如果
mapDispatchToProps
是一个函数,它可以传入dispatch
,ownProps
, 定义UI组件如何发出action
,实际上就是要调用dispatch
这个方法
mapStateToProps
:建立一个redux的state对象到UI组件的Props对象的映射关系mapDispatchToProps
:建立UI组件的参数到store.dispatch方法的映射。也就是说,定义了那些用户的操作应该被当成action,传给store
<Provider>
组件
connect
方法生成容器组件以后,需要让容器组件拿到state
对象,才能生成 UI 组件的参数。
一种解决方法是将state
对象作为参数,传入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级级将state
传下去就很麻烦。
React-Redux 提供Provider
组件,可以让容器组件拿到state
。
1 | import { Provider } from 'react-redux' |
上面代码中,Provider
在根组件外面包了一层,这样一来,App
的所有子组件就默认都可以拿到state
了。
它的原理是
React
组件的context
属性,请看源码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 class Provider extends Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
Provider.childContextTypes = {
store: React.PropTypes.object
}
上面代码中,store
放在了上下文对象context
上面。然后,子组件就可以从context
拿到store
,代码大致如下。
1 | class VisibleTodoList extends Component { |
实例:计数器
我们来看一个实例。下面是一个计数器组件,它是一个纯的 UI 组件。
1
2
3
4
5
6
7
8
9
10
11 class Counter extends Component {
render() {
const { value, onIncreaseClick } = this.props
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
)
}
}
上面代码中,这个 UI 组件有两个参数:value
和onIncreaseClick
。前者需要从state
计算得到,后者需要向外发出 Action。
接着,定义value
到state
的映射,以及onIncreaseClick
到dispatch
的映射。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 function mapStateToProps(state) {
return {
value: state.count
}
}
function mapDispatchToProps(dispatch) {
return {
onIncreaseClick: () => dispatch(increaseAction)
}
}
// Action Creator
const increaseAction = { type: 'increase' }
然后,使用connect
方法生成容器组件。
1
2
3
4 const App = connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
然后,定义这个组件的 Reducer。
1
2
3
4
5
6
7
8
9
10 // Reducer
function counter(state = { count: 0 }, action) {
const count = state.count
switch (action.type) {
case 'increase':
return { count: count + 1 }
default:
return state
}
}
最后,生成store
对象,并使用Provider
在根组件外面包一层。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import { loadState, saveState } from './localStorage';
const persistedState = loadState();
const store = createStore(
todoApp,
persistedState
);
store.subscribe(throttle(() => {
saveState({
todos: store.getState().todos,
})
}, 1000))
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
bindActionCreators
- 把 action creators 转成拥有同名 keys 的对象,但使用 dispatch 把每个 action creator 包围起来,这样可以直接调用它们。
1 | const mapDispatchToProps = (dispatch) => { |
1 | const mapDispatchToProps = (dispatch) => { |
上面的代码有什么区别?
1 | // TodoActionCreators.js |
1 | // 针对TodoActionCreators不使用bindActionCreators的话,我们需要这样写: |
Action和reducer的连接
store.dispatch(action)
是 View 发出 Action 的唯一方法。如下:
1
2
3
4
5
6
7
8
9store.dispatch(addTodo('Learn Redux'));
// 把store作为第一参数(store)传给reducer
// 把addTodo('Learn Redux')作为第二参数(action)传给recuder
// reducer
const reducer = function (state, action) {
// ...
return new_state;
};下面是一个实际的例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14const defaultState = 0;
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD':
return state + action.payload;
default:
return state;
}
};
const state = reducer(1, {
type: 'ADD',
payload: 2
});
props和state
state
的主要作用是用于组件保存、控制、修改自己的可变状态。state
在组件内部初始化,可以被组件自身修改,而外部不能访问也不能修改。你可以认为 state
是一个局部的、只能被组件自身控制的数据源。state
中状态可以通过 this.setState
方法进行更新,setState
会导致组件的重新渲染。
props
的主要作用是让使用该组件的父组件可以传入参数来配置该组件。它是外部传进来的配置参数,组件内部无法控制也无法修改。除非外部组件主动传入新的 props
,否则组件的 props
永远保持不变。
state
和 props
有着千丝万缕的关系。它们都可以决定组件的行为和显示形态。一个组件的 state
中的数据可以通过 props
传给子组件,一个组件可以使用外部传入的 props
来初始化自己的 state
。但是它们的职责其实非常明晰分明:**state
是让组件控制自己的状态,props
是让父组件对自己进行配置**。
如果你觉得还是搞不清 state
和 props
的使用场景,那么请记住一个简单的规则:尽量少地用 state
,尽量多地用 props
。
没有 state
的组件叫无状态组件(stateless component),设置了 state 的叫做有状态组件(stateful component)。因为状态会带来管理的复杂性,我们尽量多地写无状态组件,尽量少地写有状态的组件。这样会降低代码维护的难度,也会在一定程度上增强组件的可复用性。前端应用状态管理是一个复杂的问题,我们后续会继续讨论。
React.js 非常鼓励无状态组件,在 0.14 版本引入了函数式组件——一种定义不能使用 state
组件,例如一个原来这样写的组件:
1 | class HelloWorld extends Component { |
用函数式组件的编写方式就是:
1 | const HelloWorld = (props) => { |
以前一个组件是通过继承 Component
来构建,一个子类就是一个组件。而用函数式的组件编写方式是一个函数就是一个组件,你可以和以前一样通过 <HellWorld />
使用该组件。不同的是,函数式组件只能接受 props
而无法像跟类组件一样可以在 constructor
里面初始化 state
。你可以理解函数式组件就是一种只能接受 props
和提供 render
方法的类组件。
示例
1 | // Link需要父组件传入active,children和onClick三个属性 |
1 | // Link的容器组件通过mapStateToProps赋予active属性,通过mapDispatchToProps赋予onClick属性 |
1 | // 调用方给FilterLink赋予children属性(ALL,Active,Completed) |
示例2
1 | // components/TodoList.js |
mapStateToProps的全称是
map redux's state to this UIcomponent props
,也就是说,先拿取redux中的state,然后进行一些函数的处理,返回出state。然后将这个state传给UI组件所以下面的代码执行顺序是:
1
2
3{todos.map(todo => (
<Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
))}- mapStateToProps返回todos变量,赋给UI组件的todos这个props
- todos是一个array,遍历取出单个元素,得到todo.id
- mapDispatchToProps返回toggleTodo交给UI组件的toggleTodo变量这个props
- 调用toggleTodo(todo.id)
示例 3
1 | // containers/AddTodo.js |
- 一个UI组件,要想获取dispatch能力的话,就可以使用
connect()(AddTodo)
- 这样,就可以通过
const { dispatch } = this.props
获取dispatch函数