概述
- redux是一个用于JS状态容器,提供可预测化的状态管理
- redux可以让你够贱一致化的应用,运行于不同的环境,并且易于测试
- 在这里我们首先要明白的是什么叫可预测?什么叫状态容器?
- 什么叫状态?实际上就是变量,对话框显示或隐藏的变量,一杯奶茶多少钱的变量。那么这个状态容器,实际上就是一个存放这些变量的变量。
- 你创建了一个全局变量叫Store,然后将代码中控制各个状态的变量存放在里面,那么现在Store就叫做状态容器。
- 什么叫可预测?
- 你在操作这个Store的时候,总是用Store.price的方式来设置值,这种操作数据的方式很原始,对于复杂的系统而言永远都不知道程序在运行的过程中发生了什么。
- 那么现在我们都通过发送一个Action去做修改,而Store在接收到Action后会使用Reducer对Action传递的数据做处理,最后应用到Store中。
- 相对于Store.price的方式来修改者,这种方式无疑更麻烦,但是这种方式的好处就是,每一个Action里面都可以写日志,可以记录各种状态的变动,这就是可预测。
- 所以如果你的程序很简单,你完全没有必要去用Redux。
Redux工作流

mapStateToProps和mapDispatchToProps梳理
mapStateToProps(state, ownProps):
- mapStateToProps是一个函数,用于建立组件跟 store 的 state 的映射关系
- 作为一个函数,它可以传入两个参数,结果一定要返回一个 object。
- 传入mapStateToProps之后,会订阅store的状态改变,在每次 store 的 state 发生变化的时候,都会被调用
- ownProps代表组件本身的props,如果写了第二个参数ownProps,那么当prop发生变化的时候,mapStateToProps也会被调用。例如,当 props接收到来自父组件一个小小的改动,那么你所使用的ownProps 参数,mapStateToProps 都会被重新计算)。
- mapStateToProps可以不传,如果不传,组件不会监听store的变化,也就是说Store的更新不会引起UI的更新
Redux的中间件
Redux是个可预测的状态容器,这个可预测在于对数据的每一次修改都可以进行相应的处理和记录。
假如现在我们需要在每次修改数据时,记录修改的内容,我们可以在每一个dispatch前面加上一个console.info记录修改的内容。
但是这样太繁琐了,所以我们可以直接修改store.dispatch:
1 2 3 4 5
| let next = store.dispatch store.dispatch = (action)=> { console.info('修改内容为:', action) next(action) }
|
Redux中也有同样的功能,那就是applyMiddleware。直译过来就是“应用中间件”,它的作用就是改造dispatch函数,跟上面的玩法基本雷同。
- applyMiddleware的作用:修改store.dipatch的行为
1 2 3 4 5 6 7
| import { createStore, applyMiddleware } from 'redux'; import reducer from './reducers';
const store = createStore(reducer, applyMiddleware(curStore => next => action => { console.info(curStore.getState(), action); return next(action); }));
|
redux-logger:日志中间件
通常我们没有必要自己写中间件,比如日志的记录就已经有了成熟的中间件:redux-logger
,这里给一个简单的例子:
1 2 3 4 5 6 7 8 9 10
| import { applyMiddleware, createStore } from 'redux'; import createLogger from 'redux-logger'; import reducer from './reducers';
const logger = createLogger();
const store = createStore( reducer, applyMiddleware(logger) );
|
这样就可以记录所有action及其发送前后的state的日志
redux-thunk:处理异步action
下面代码点击按钮后,直接修改了按钮的文本,这个文本是个固定的值。
1 2 3 4 5 6 7 8 9
| export const CHANGE_BTN_TEXT = 'CHANGE_BTN_TEXT';
export const changeBtnText = (text) => { return { type: T.CHANGE_BTN_TEXT, payload: text }; };
|
但是在我们实际生产的过程中,很多情况都是需要去请求服务端拿到数据再修改的,这个过程是一个异步的过程。又或者需要setTimeout去做一些事情。
1 2 3 4 5 6 7 8 9 10 11 12
| const mapDispatchToProps = (dispatch) => { return { changeText: (text) => { dispatch(changeBtnText('正在加载中')); axios.get('http://test.com').then(() => { dispatch(changeBtnText('加载完毕')); }).catch(() => { dispatch(changeBtnText('加载有误')); }); } }; };
|
- 问题来了,当请求后台的过程非常快,导致加载完毕先出现,然后再展示加载中。
- 这个时候我们需要去通过store.getState获取当前状态,从而判断到底是展示正在加载中还是展示加载完毕。
- 这个过程就不能放在mapDispatchToProps中了,而需要放在中间件中,因为中间件中可以拿到store。
1 2 3 4 5 6 7 8 9
| import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import reducer from './reducers';
const store = createStore( reducer, applyMiddleware(thunk) );
|
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
| import axios from 'axios'; import * as T from './actionTypes';
export const changeBtnText = (text) => { return { type: T.CHANGE_BTN_TEXT, payload: text }; };
export const changeBtnTextAsync = (text) => { return (dispatch, getState) => { if (!getState().isLoading) { dispatch(changeBtnText('正在加载中')); } axios.get(`http://test.com/${text}`).then(() => { if (getState().isLoading) { dispatch(changeBtnText('加载完毕')); } }).catch(() => { dispatch(changeBtnText('加载有误')); }); }; };
const mapDispatchToProps = (dispatch) => { return { changeText: (text) => { dispatch(changeBtnTextAsync(text)); } }; };
|
thunk的源码超级简单:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; }
const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
|
createThunkMiddleware加强了dispatch的功能,在dispatch一个action之前,去判断action是否是一个函数,如果是函数,那么就执行这个函数。
通过redux-thunk我们可以简单地进行异步操作,并且可以获取到各个异步操作时期状态的值。
redux-actions:简化redux的使用
简化工作主要集中在构造action和处理reducers方面。
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
| import axios from 'axios'; import * as T from './actionTypes';
export const changeBtnText = (text) => { return { type: T.CHANGE_BTN_TEXT, payload: text }; };
export const changeBtnTextAsync = () => { return (dispatch, getState) => { if (!getState().isLoading) { dispatch(changeBtnText('正在加载中')); } axios.get('http://test.com').then(() => { if (getState().isLoading) { dispatch(changeBtnText('加载完毕')); } }).catch(() => { dispatch(changeBtnText('加载有误')); }); }; };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import axios from 'axios'; import * as T from './actionTypes'; import { createAction } from 'redux-actions';
export const changeBtnText = createAction(T.CHANGE_BTN_TEXT, text => text);
export const changeBtnTextAsync = () => { return (dispatch, getState) => { if (!getState().isLoading) { dispatch(changeBtnText('正在加载中')); } axios.get('http://test.com').then(() => { if (getState().isLoading) { dispatch(changeBtnText('加载完毕')); } }).catch(() => { dispatch(changeBtnText('加载有误')); }); }; };
|
- 异步的action就不要用createAction,因为这个createAction返回的是一个对象,而不是一个函数,就会导致redux-thunk的代码没有起到作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import * as T from './actionTypes';
const initialState = { btnText: '我是按钮', };
const pageMainReducer = (state = initialState, action) => { switch (action.type) { case T.CHANGE_BTN_TEXT: return { ...state, btnText: action.payload }; default: return state; } };
export default pageMainReducer;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { handleActions } from 'redux-actions'; import * as T from './actionTypes';
const initialState = { btnText: '我是按钮', };
const pageMainReducer = handleActions({ [T.CHANGE_BTN_TEXT]: { next(state, action) { return { ...state, btnText: action.payload, }; }, throw(state) { return state; }, }, }, initialState);
export default pageMainReducer;
|
redux-promise:redux-actions的好基友,轻松创建和处理异步action
- 上面在使用redux-actions的createAction时,我们对异步的action无法处理。
- 因为我们使用createAction后返回的是一个对象,而不是一个函数,就会导致redux-thunk的代码没有起到作用。
- 而现在我们将使用redux-promise来处理这类情况。
1 2
| export const changeBtnText = createAction(T.CHANGE_BTN_TEXT, text => text);
|
1 2 3 4 5 6 7
| import thunk from 'redux-thunk'; import createLogger from 'redux-logger'; import promiseMiddleware from 'redux-promise'; import reducer from './reducers';
const store = createStore(reducer, applyMiddleware(thunk, createLogger, promiseMiddleware));
|
1 2 3 4
| export const changeBtnTextAsync = createAction(T.CHANGE_BTN_TEXT_ASYNC, (text) => { return axios.get(`http://test.com/${text}`); });
|
可以看到我们这里返回的是一个Promise对象.(axios的get方法结果就是Promise对象)
我们还记得redux-thunk中间件,它会去判断action是否是一个函数,如果是就执行。
而我们这里的redux-promise中间件,他会在dispatch时,判断如果action不是类似
1 2 3 4
| { type:'', payload: '' }
|
这样的结构,也就是 FSA,那么就去判断是否为promise对象,如果是就执行action.then的玩法。
很明显,我们createAction后的结果是FSA,所以会走下面这个分支,它会去判断action.payload是否为promise对象,是的话那就
1 2 3 4 5 6
| action.payload .then(result => dispatch({ ...action, payload: result })) .catch(error => { dispatch({ ...action, payload: error, error: true }); return Promise.reject(error); })
|
也就是说我们的代码最后会转变为:
1 2 3 4 5 6
| axios.get(`http://test.com/${text}`) .then(result => dispatch({ ...action, payload: result })) .catch(error => { dispatch({ ...action, payload: error, error: true }); return Promise.reject(error); })
|
react-redux性能优化之reselect
selector 的作用:将多个 state 进行计算后生成新的 state
性能问题
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 45 46
| class App extends Component { static propTypes = {}; render() { const { visibleTodos, visibilityFilter, onAddClick, onTodoClick, onFilterChange } = this.props; return ( <div> <AddTodo onAddClick={(text) => onAddClick(text)} />
<TodoList todos={visibleTodos} onTodoClick={(index) => onTodoClick(index)} />
<Footer filter={visibilityFilter} onFilterChange={(nextFilter) => onFilterChange(nextFilter)} /> </div> ); } }
export const selectTodos = (todos, filter) => { switch (filter) { case 'SHOW_ALL': return todos case 'SHOW_COMPLETED': return todos.filter(todo => todo.completed) case 'SHOW_ACTIVE': return todos.filter(todo => !todo.completed) } }
const mapStateToProps = (state) => ({ visibleTodos: selectTodos(state.todos, state.visibilityFilter), visibilityFilter: state.visibilityFilter });
const mapDispatchToProps = (dispatch) => ({ onAddClick: (text) => dispatch(addTodo(text)), onTodoClick: (index) => dispatch(toggleTodo(index)), onFilterChange: (nextFilter) => dispatch(setVisibilityFilter(nextFilter)) });
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
connect 函数实现的时候,我们知道映射 props
的函数被 store.subscribe()
了,因此每次组件更新的时候,无论 state 是否改变,都会调用 mapStateToProps
,而 mapStateToProps
在计算 state 的时候就会调用 state 计算函数,过程 如下:
store.subscribe()
(注册事件)
- 状态更新时调用
mapStateToProps
(一个selector,返回 state)
- 调用 state 计算函数
selectTodos
那么,问题 来了,如果 selector 的计算量比较大,每次更新的重新计算就会造成性能问题。而解决性能问题的 出发点 就是:避免不必要的计算。
解决问题的方式:从 selector 着手,即 mapStateToProps
,如果 selector 接受的状态参数不变,那么就不调用计算函数,直接利用之前的结果。
- reselect 其实就是 redux 的一个中间件,它通过计算获得新的 state,然后传递到 Redux Store。
- 其主要就是进行了中间的那一步计算,使得计算的状态被缓存,从而根据传入的 state 判断是否需要调用计算函数,而不用在组件每次更新的时候都进行调用,从而更加高效。
不使用Selector:

使用Selector:

采用 reselect,更新 App 组件
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 45 46 47 48
|
const getTodos = (state) => state.todos; const getVisibilityFilter = (state) => state.visibilityFilter;
export const selectTodos = (todos, filter) => { switch (filter) { case 'SHOW_ALL': return todos case 'SHOW_COMPLETED': return todos.filter(todo => todo.completed) case 'SHOW_ACTIVE': return todos.filter(todo => !todo.completed) } }
const getVisibleTodos = createSelector( [getTodos, getVisibilityFilter], selectTodos );
const mapStateToProps = (state) => ({ visibleTodos: getVisibleTodos(state), visibilityFilter: state.visibilityFilter });
const mapDispatchToProps = (dispatch) => ({ onAddClick: (text) => dispatch(addTodo(text)), onTodoClick: (index) => dispatch(toggleTodo(index)), onFilterChange: (nextFilter) => dispatch(setVisibilityFilter(nextFilter)) });
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
解释
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
| import { connect } from 'react-redux' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList'
const getVisibleTodos = (todos, filter) => { switch (filter) { case 'SHOW_ALL': return todos case 'SHOW_COMPLETED': return todos.filter(t => t.completed) case 'SHOW_ACTIVE': return todos.filter(t => !t.completed) } }
const mapStateToProps = (state) => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } }
const mapDispatchToProps = (dispatch) => { return { onTodoClick: (id) => { dispatch(toggleTodo(id)) } } }
const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList)
export default VisibleTodoList
|
在上面的例子中,mapStateToProps
调用getVisibleTodos
去计算todos
.这个函数设计的是相当好的,
但是有个缺点:todos
在每一次组件更新的时候都会重新计算.如果state树的结构比较大,或者计算比较昂贵,每一次组件更新的时候都进行计算的话,将会导致性能问题.
Reselect
能够帮助redux来避免不必要的重新计算过程.
我们可以使用记忆缓存selector代替getVisibleTodos
,如果state.todos
和state.visibilityFilter
发生变化,他会重新计算state
,但是发生在其他部分的state变化,就不会重新计算.
Reslect提供一个函数createSelector
来创建一个记忆selectors.
createSelector
接受一个input-selectors
和一个变换函数作为参数.
- 如果Redux的state发生改变造成
input-selector
的值发生改变,selector会调用变换函数,依据input-selector
做参数,返回一个结果.
- 如果
input-selector
返回的结果和前面的一样,那么就会直接返回有关state,会省略变换函数的调用.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { createSelector } from 'reselect'
const getVisibilityFilter = (state) => state.visibilityFilter const getTodos = (state) => state.todos
export const getVisibleTodos = createSelector( [ getVisibilityFilter, getTodos ], (visibilityFilter, todos) => { switch (visibilityFilter) { case 'SHOW_ALL': return todos case 'SHOW_COMPLETED': return todos.filter(t => t.completed) case 'SHOW_ACTIVE': return todos.filter(t => !t.completed) } } )
|
上面的的实例中,getVisibilityfilter
和getTodos
是input-selectors.这两个函数是普通的非记忆selector函数,因为他们没有变换他们select的数据.
getVisibleTodos
另一方面是一个记忆selector.他接收getVisibilityfilter
和getTodos
作为input-selectors,并且作为一个变换函数计算筛选的todo list.
聚合selectors
一个记忆性selector本身也可以作为另一个记忆性selector的input-selector.这里getVisibleTodos
可以作为input-selector作为关键字筛选的input-selector:
1 2 3 4 5 6 7 8
| const getKeyword = (state) => state.keyword
const getVisibleTodosFilteredByKeyword = createSelector( [ getVisibleTodos, getKeyword ], (visibleTodos, keyword) => visibleTodos.filter( todo => todo.text.indexOf(keyword) > -1 ) )
|
连接一个Selector到Redux Store
如果你正在使用 React Redux, 你可以直接传递selector到 mapStateToProps()
:
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
| import { connect } from 'react-redux' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList' import { getVisibleTodos } from '../selectors'
const mapStateToProps = (state) => { return { todos: getVisibleTodos(state) } }
const mapDispatchToProps = (dispatch) => { return { onTodoClick: (id) => { dispatch(toggleTodo(id)) } } }
const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList)
export default VisibleTodoList
|
总结
- redux是一个可预测的状态容器,
- react-redux是将store和react结合起来,使得数据展示和修改对于react项目而言更简单
- redux中间件就是在dispatch action前对action做一些处理
- redux-thunk用于对异步做操作
- redux-actions用于简化redux操作
- redux-promise可以配合redux-actions用来处理Promise对象,使得异步操作更简单