概述

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

Redux工作流

123

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
// action.js
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
// 首先创造store的时候需要应用react-thunk
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
// 先来看看原先的actions
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
// 先来看看原先的reducers
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
// 使用handleActions来处理
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
// createAction
export const changeBtnText = createAction(T.CHANGE_BTN_TEXT, text => text);
1
2
3
4
5
6
7
// 加入redux-promise中间件
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
// 再处理异步action:
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>
);
}
}

// 一个 state 计算函数
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)
}
}

// mapStateToProps 就是一个 selector,每次组件更新的时候就会被调用
// 【缺点】每次组件更新的时候都会重新计算 visibleTodos,如果计算量比较大,会造成性能问题
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 是否改变,都会调用 mapStateToPropsmapStateToProps 在计算 state 的时候就会调用 state 计算函数,过程 如下:

  1. store.subscribe()(注册事件)
  2. 状态更新时调用 mapStateToProps(一个selector,返回 state)
  3. 调用 state 计算函数 selectTodos

那么,问题 来了,如果 selector 的计算量比较大,每次更新的重新计算就会造成性能问题。而解决性能问题的 出发点 就是:避免不必要的计算

解决问题的方式:从 selector 着手,即 mapStateToProps如果 selector 接受的状态参数不变,那么就不调用计算函数,直接利用之前的结果。

  • reselect 其实就是 redux 的一个中间件,它通过计算获得新的 state,然后传递到 Redux Store。
  • 其主要就是进行了中间的那一步计算,使得计算的状态被缓存,从而根据传入的 state 判断是否需要调用计算函数,而不用在组件每次更新的时候都进行调用,从而更加高效。

不使用Selector:

WechatIMG55

使用Selector:

WechatIMG56

采用 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
// 不用 reselect 的缺点:每次组件更新的时候都会重新计算 visibleTodos
/*const mapStateToProps = (state) => ({
visibleTodos: selectTodos(state.todos, state.visibilityFilter),
visibilityFilter: state.visibilityFilter
});*/

// 采用 reselect 后,相当于记忆缓存,会缓存状态
// 如果 state.todos 和 state.visibilityFilter 发生变化,它会重新计算 state
// 但是发生在其他部分的 state 变化,就不会重新计算
const getTodos = (state) => state.todos;
const getVisibilityFilter = (state) => state.visibilityFilter;

// reducer
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)
}
}

// getTodos 和 getVisibilityFilter 返回的参数将传入 selectTodos
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))
});


/**
* connect用法:connect(selectors)(App); // selectors 即是一个对象,包含了状态属性和方法
* connect作用:即使 Dumb 组件从 store 中获取数据
*/
// export default connect(mapStateToProps)(App);
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'

//下面这段代码是根据过滤器的state来改变日程state的函数
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是根据过滤函数返回的state,传入两个实参
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
//mapDispatchToProps来传递dispatch的方法
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
//使用Redux的connect函数注入state,到TodoList组件
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)

export default VisibleTodoList
  • 在上面的例子中,mapStateToProps调用getVisibleTodos去计算todos.这个函数设计的是相当好的,

  • 但是有个缺点:todos在每一次组件更新的时候都会重新计算.如果state树的结构比较大,或者计算比较昂贵,每一次组件更新的时候都进行计算的话,将会导致性能问题.

  • Reselect能够帮助redux来避免不必要的重新计算过程.

  • 我们可以使用记忆缓存selector代替getVisibleTodos,如果state.todosstate.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)
    }
    }
    )
    • 上面的的实例中,getVisibilityfiltergetTodos是input-selectors.这两个函数是普通的非记忆selector函数,因为他们没有变换他们select的数据.

    • getVisibleTodos另一方面是一个记忆selector.他接收getVisibilityfiltergetTodos作为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对象,使得异步操作更简单