透过TodoMVC看Redux

今天再次review了Redux文档中TodoMVC的示例,通过这个示例,我认为能较好得反应出Redux的完整思想。这里做些简要分析,同时对于最基本的Redux做些总结吧。

store、state、reducer与action

pic-1

store是Redux的核心,与Flux不同的是,store在Redux中是唯一的,可谓是中枢系统。

在todoMVC中,我们首先来创建store,这里尽可能简化代码,只为说明核心问题:

1
2
3
4
5
6
7
import { createStore } from 'redux'
import rootReducer from '../reducers'
export default function configureStore(initialState) {
const store = createStore(rootReducer, initialState)
return store
}

createStore是redux的API,其中第一个参数为reducer,其主要接受两个参数,分别是当前的state与相应action,返回新的state。那让我们来看一下rootReducer:

1
2
3
4
5
6
7
8
import { combineReducers } from 'redux'
import todos from './todos'
const rootReducer = combineReducers({
todos
})
export default rootReducer

这里使用了一个辅助函数combineReducers,主要应用于各个reducer的合并。因为当应用复杂之后,需要拆分reducer,分别负责不同的state管理。似乎这里并不能说明问题,只有一个reducer: todos:

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
const initialState = [
{
text: 'Use Redux',
completed: false,
id: 0
}
]
export default function todos(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return [
{
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
completed: false,
text: action.text
},
...state
]
case DELETE_TODO:
case EDIT_TODO:
...
default:
return state
}
}

对于reducer而言,传入的是previousState与action,返回新的newState。

这里是使用了较多ES6的,当传入的state为undefined时,即首次执行时,传入state为默认初始state。当action为ADD_TODO,返回新的一条todo + 传入state,使用了扩展运算符(…)。

到这里为止,TodoMVC示例的Redux核心思想部分已经梳理完了,这部分其实并不是很多,关键在于理解吧。当然这还远远没有结束,Redux实则为一种库,可以与Backbone、Angular等多种前端框架相配合,毫无疑问它与React的契合度更高。React把处理state中数据的问题留给了我们,Redux就是为了帮我们解决这个问题。

React与Redux

在React-Redux中,分为容器组件(最顶层)和展示组件(中间和子组件),容器组件使用Redux,而展示组件则是普通的React,利用props。

那么实际中,我们是如何来连接React与Redux store的呢?让我们来看一下TodoMVC的顶层结构:

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './containers/App'
import configureStore from './store/configureStore'
const store = configureStore()
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

containers/App.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
import React, { Component, PropTypes } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import Header from '../components/Header'
import MainSection from '../components/MainSection'
import * as TodoActions from '../actions'
class App extends Component {
render() {
const { todos, actions } = this.props
return (
<div>
<Header addTodo={actions.addTodo} />
<MainSection todos={todos} actions={actions} />
</div>
)
}
}
function mapStateToProps(state) {
return {
todos: state.todos
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(TodoActions, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App)

连接主要通过connect方法来实现,在App类中,我们发现this.props中含有todos与actions,这两者正来自于connect中注入的两个函数的返回值。

mapStateToProps(state)将会监听Redux store的变化,一旦其发生改变,mapStateToprops将会被调用,新的state.todos将用于赋值,返回对象与App的this.props合并。

mapDispatchToProps(dispatch)这里使用了一个辅助函数bindActionCreators(actionCreators, dispatch),这样处理的目的在于将dispatch与actionCreators绑定在一起后作为actions传入props。比如当调用actions.addTodo时,便会自行触发dispatch(action),完成Redux的一系列流程。

这里的主要目的就在于,将Redux store中的state与dispatch进行分离,分别传入props。当然我们还需要在根组件上注入Redux store,store创建完成后,采用<Provider store={store}></Provider>将整个视图结构包装在内部。

具体实现

blog-17-pic-2

关于整个TodoMVC的具体实现,大家可以参考上面的组件结构图,或者直接看官方代码,这里就不详细展开了。