前言
这篇文章是我在学习redux的过程中,在编码的每一个阶段编写的学习笔记。项目地址:todo App。纯react是在normal分支,结合redux的是在redux分支。react(15.6.1)和redux(3.7.2)都是当前(2017.8.4)的最新版本。所有的数据是存储在localstorage下的,点击查看demo。因为我的ui是移动端的,pc端没有做处理,只能在移动端或是chrome下按CTRL+SHIFT+M进行查看。
演示
概念
action 来描述“发生了什么”
reducers 来根据 action 更新 state
Store 就是把它们联系到一起的对象
action
Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。
// action types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const CHANGE_TAB = 'CHANGE_TAB';
const DELETE_TODO = 'DELETE_TODO';
// action constants
export const pageFilters = {
GO_LIST: 'GO_LIST',
GO_FORM: 'GO_FORM',
};
在 Redux 中的 action creators只是简单的返回一个 action:
// action creators
// 添加一个todo任务
export function addTodo(newTodo) {
return { type: ADD_TODO, newTodo };
}
// 勾选完成/未完成的切换
export function toggleTodo(index) {
return { type: TOGGLE_TODO, index };
}
// 列表页与表单页的切换
export function changeTab(filter) {
return { type: CHANGE_TAB, filter };
}
// 删除一个todo
export function deleteTodo(index) {
return { type: DELETE_TODO, index };
}
Reducer
Action 只是描述了有事情发生了这一事实,并没有指明应用如何更新 state。而这正是 reducer 要做的事情。
先设计一个state tree,用来存放所有的state。
const stateTree = {
pageFilter: 'GO_LIST', //当前页
todos: [ //任务列表
{
text: '11',
place: '1',
time: '2:20',
content: 'dgusdguysdgisdhuis',
completed: false,
},
],
};
构造一个初始化state
const initialState = {
pageFilter: pageFilters.GO_LIST,
todos: [],
};
创建reducer。通过action的type改变state
// reducer一定为纯函数,接受state和action,返回state。如果不存在state,则返回最初的state。
// (previousState, action) => newState
function todoApp(state = initialState, action) {
switch (action.type) {
case CHANGE_TAB:
// 这里不能直接修改state,要使用 Object.assign() 新建了一个副本
return Object.assign({}, state, {
filter: action.filter,
});
default:
return state;
}
}
写一下对其他action的处理,值得注意的是,都不要在原来的state上进行操作!!
function todoApp(state = initialState, action) {
switch (action.type) {
case CHANGE_TAB:
return Object.assign({}, state, {
filter: action.filter,
});
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
action.newTodo,
],
});
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if (action.index === index) {
return Object.assign({}, todo, {
completed: !todo.completed,
});
}
return todo;
}),
});
case DELETE_TODO:
return Object.assign({}, state, {
todos: [].concat(state.todos).splice(action.index, 1),
});
default:
return state;
}
}
除了CHANGE_TAB
其他的action都是用来操作todos的,把它们拆分出来,使代码结构看起来更加清晰。
// 操作state.todos
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state.todos,
action.newTodo,
];
case TOGGLE_TODO:
return state.todos.map((todo, index) => {
if (action.index === index) {
return Object.assign({}, todo, {
completed: !todo.completed,
});
}
return todo;
});
case DELETE_TODO:
return [].concat(state.todos).splice(action.index, 1);
default:
return state;
}
}
现在我们可以开发一个函数来做为主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 并不需要设置初始化时完整的 state。初始时,如果传入 undefined, 子 reducer 将负责返回它们的默认值。
function todoApp(state = {}, action) {
return {
filter: filter(state.filter, action),
todos: todos(state.todos, action),
};
}
调用redux的工具类combineReducers
把子reducer整合在一起。
const todoApp = combineReducers({
filter,
todos,
});
Store
Store 有以下职责:
- 维持应用的 state;
- 提供 getState() 方法获取 state;
- 提供 dispatch(action) 方法更新 state;
- 通过 subscribe(listener) 注册监听器;
- 通过 subscribe(listener) 返回的函数注销监听器。
创建一个store
import { createStore } from 'redux';
import todoApp from './reduces';
// Redux应用只有一个单一的store
// 根据已有的reducer创建store
// createStore第二个参数是可选的, 用于设置 state 初始状态
const store = createStore(todoApp);
发起action
// 打印初始state状态
console.log(store.getState());
// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
const unsubscribe = store.subscribe(() =>
console.log(store.getState()),
);
// 发起action
store.dispatch(changeTab(GO_FORM));
// 停止监听 state 更新
unsubscribe();
可以从console中看到state的变化。
搭配 React
react与redux并没有什么依赖关系,只不过react允许使用state函数的形式来描述界面。
先安装react-redux
,使用<Provider>
把根组件包起来
import { Provider } from 'react-redux';
render(
<Provider store={store}>
<Root />
</Provider>
,
document.getElementById('root'),
);
Provider中的任何一个组件,想要使用store中的数据,就必须connect()
对其封装。
connect
connect() 接收四个参数,它们分别是 mapStateToProps,mapDispatchToProps,mergeProps和options。
mapStateToProps 读取state
在这里我们使用mapStateToProps
这个函数,将store中的数据作为props绑定到组件上。
// 构造一个函数返回需要的state
const mapStateToProps = (state) => {
const { filter, todos } = state;
return {
filter,
todos,
};
};
// 使用connect将state作为props传入root中
export default connect(mapStateToProps)(Root);
这时,<Root/>
中的就有了如下的props
{
dispatch:function dispatch(action),
filter:"GO_LIST",
todos:Array(0)
}
分发action
定义mapDispatchToProps()
方法接受dispatch()
方法,并返回一个回调函数,用来分发action。
// 构造一个函数来分发action
const mapDispatchToProps = dispatch => ({
goPages: (pageName) => {
dispatch(changeTab(pageName));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Root);
这时再打印一下<Root/>
里的props
{
filter:"GO_LIST",
goPages:pageName => {…},
todos:Array(0)
}
在调用时,作为props传递给子组件,如:
<AddPage changeTab={() => goPages(pageFilters.GO_LIST)} data={this.state.data} />