前言
redux
作为react
的状态管理工具,让很多开发者敬而远之,主要是因为它比较繁杂的用法还有各种组成部分,像Store
、Reducer
等。这次毕设恰好用到了redux
来进行项目的状态管理,使得程序变得更加优雅,于是趁此机会总结一下。
什么情况下需要用redux
实际上,大多数情况下,我们不需要用到redux
,因为实际的应用场景没有复杂到需要用redux
。
所以,如果你的UI层很简单,没有很多互动,redux
就是没有必要的,用了反而会增加复杂性。
那在什么情况下需要用redux
呢?在多交互、多数据源的场景下需要用到redux
,例如:
- 用户的使用方式复杂
- 不同身份的用户有不同的使用方式
- 多个用户之间可以协作
- 与服务器大量交互,或者使用了
WebSocket
View
要从多个来源获取数据
在我的项目中,考虑使用redux
的场景是这样的:
场景:在诗词页面向后台请求诗词的信息,点击诗词页面的某个按钮,会跳转到另一个页面,这个页面也需要用到这个诗词的信息。
常用的做法是:在两个页面
componentWillMount
生命周期里向后台请求诗词信息。但这种做法的缺点是多次向后台发送请求,会造成页面加载过慢,性能较差等问题。
此时就可以考虑使用
redux
:在诗词页面向后台请求到诗词的信息时,将这个信息存储在redux
的store
中,跳转到另一个页面时,直接从store
里获取这个诗词信息即可。这样可以减少http
请求的次数。
因此,我们可以根据自己的实际情况选择是否要使用redux
。
设计思想
redux
的设计思想可以总结为:
- Web应用时一个状态机,视图与状态是一一对应的。
- 所有的状态,保存在一个对象里面。
视图与状态是一一对应意味着:状态改变会导致视图改变。
所有的状态都保存在一个对象里面,这个对象就是之后会提到的Store。
基本概念与API
此处参照阮一峰老师的博客。
Store
Store
就是保存数据的地方,可以把它看成一个容器,整个应用只能有一个Store。
redux
提供createStore
这个函数,用来生成Store
:
import { createStore } from 'redux';
const store = createStore(fn);
State
Store
对象包含所有数据。如果想要得到某个时间节点的数据,就要对Store
生成快照。这种时间节点的数据集合,就叫做State
。
redux
规定,一个State
对应一个View
。只要State
相同,View
就相同。
Action
State
的变化,会导致View
的变化。但是用户接触不到State
,只能接触到View
。所以State
的变化必须是View
导致的,Action
就是View
向State
发出的通知,表示State
要变化了。
Action
必须是一个对象,且其中的type
属性必须声明,表示Action
的名称。其他属性可以自由设置,详情可见社区。
Action Creator
Action
是一个对象,我们必须要在一开始就定义好这个Action
的type
和data
,但一般data
是在程序运行过程中才获取到(比如从后台获取到数据),赋值给Action
,所以可以定义一个函数来生成Action
,这个函数就叫做Action Creator
。
export const changeAudioInfo = (data) => ({
type: GET_AUDIO_INFO,
data: data, // 此处可整行简写为data此处属性写成payload或者data都可以,都表示这个action承载的数据
});
store.dispatch()
store.dispatch()
是View
发出Action
的唯一方法。
import { createStore } from 'redux';
const store = createStore(fn);
store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
});
store.dispatch
接受一个Action
对象作为参数,将它发送出去。
结合Action Creator
,代码可以写为:
store.dispatch(changeAudioInfo(data));
Reducer
Store
收到Action
以后,必须给出一个新的State
,这样View
才会发生变化。这种State
的计算过程就叫做Reducer
。
Reducer
是一个函数,它接受Action
和当前State
作为参数,返回一个新的State
。
import * as actionTypes from './constants';
import { fromJS } from 'immutable';
const defaultState = fromJS({
poemInfo: {},
authorInfo: {},
audioInfo: {},
like: false,
collect: false,
});
export default (state = defaultState, action) => {
switch (action.type) {
case actionTypes.GET_CURRENT_POEM:
return state.set('poemInfo', action.data);
case actionTypes.GET_AUTHOR_INFO:
return state.set('authorInfo', action.data);
default:
return state;
}
};
Reducer
函数不需要手动调用,store.dispatch
方法会触发Reducer
的自动执行。因此,Store
需要知道Reducer
函数,做法就是在生成Store
的时候,将Reducer
传入createStore
方法。
import { createStore } from 'redux';
const store = createStore(reducer);
这个函数之所以叫做Reducer
,是因为它可以作为数组的reduce
方法的参数:
const defaultState = 0;
// state可以看作reduce回调函数的acc,action可以看作reduce回调函数的cur
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD':
return state + action.payload;
default:
return state;
}
};
const actions = [
{ type: 'ADD', payload: 0 },
{ type: 'ADD', payload: 1 },
{ type: 'ADD', payload: 2 }
];
const total = actions.reduce(reducer, 0); // 3
Reducer
函数最重要的特征是,它是一个纯函数,同样的输入,必定得到同样的输出。由于Reducer
是纯函数,就可以保证同样的State
,必定得到同样的View
。但也因为这一点,Reducer
函数里面不能改变State
,必须返回一个全新的对象:
// State 是一个对象
function reducer(state, action) {
return Object.assign({}, state, { thingToChange });
// 或者
return { ...state, ...newState };
}
// State 是一个数组
function reducer(state, action) {
return [...state, newItem];
}
// 使用immutable数据流的话,会返回新的state对象
export default (state = defaultState, action) => {
switch (action.type) {
case actionTypes.GET_CURRENT_POEM:
return state.set('poemInfo', action.data);
case actionTypes.GET_AUTHOR_INFO:
return state.set('authorInfo', action.data);
default:
return state;
}
};
Reducer的拆分
在实际应用中,通常每个组件有自己的Reducer
,然后全局将这些Reducer
合并后再传入createStore
方法:
import { combineReducers } from 'redux-immutable';
import { reducer as searchReducer } from '@pages/Search/store/index';
import { reducer as playerReducer } from '@pages/Player/store/index';
import { reducer as poemReducer } from '@pages/Poem/store/index';
import { reducer as recordReducer } from '@pages/Record/store/index';
export default combineReducers({
search: searchReducer,
player: playerReducer,
poem: poemReducer,
record: recordReducer,
});
combineReducers()
做的就是产生一个整体的Reducer
函数。该函数根据State
的key
去执行相应的子Reducer
,并将返回结果合并成一个大的State
对象。
获取全局Store
的数据也会根据key
值来获取:
const mapStateToProps = (state) => ({
poemInfo: state.getIn(['poem', 'poemInfo']),
authorInfo: state.getIn(['poem', 'authorInfo']),
like: state.getIn(['poem', 'like']),
collect: state.getIn(['poem', 'collect']),
});
Redux的工作流程
首先,用户发出Action
——> 然后,Store
自动调用Reducer
,并传入两个参数:当前State
和收到的Action
——>Reducer
会返回新的State
——>State
一旦有变化,Store
就会调用监听函数(store.subscribe(listener)
)重新渲染View
。
中间件和异步操作
如果程序中涉及异步操作的话,我们需要使用中间件使得Reducer
在异步操作结束后自动执行。
中间件就是一个函数,对store.dispatch
方法进行了改造,在发出Action
和执行Reducer
这两步之间,添加了其他功能。
中间件的用法
比如添加输出日志功能:
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();
const store = createStore(
reducer,
applyMiddleware(logger)
);
这里有两点要注意:
createStore
方法可以接受整个应用的初始状态作为参数,那样的话,applyMiddleware
就是第三个参数:
const store = createStore(
reducer,
initial_state,
applyMiddleware(logger)
);
- 中间件的次序有讲究
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);
logger
要放在最后。
applyMiddlewares
是Redux
的原生方法,作用是将所有中间件组成一个数组,依次执行。
redux-thunk中间件
我们先看一个用dispatch
发出异步请求的例子:
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
dispatch(fetchPosts(selectedPost))
}
// ...
这个组件在componentDidMount
生命周期里执行dispatch
操作,向服务器请求数据fetchPosts(selectedPost)
。这里的fetchPosts
就是Action Creator
。
这个fetchPosts``Action Creator
是这样的:
const fetchPosts = postTitle => (dispatch, getState) => {
dispatch(requestPosts(postTitle)); // 先发出一个Action,表示操作开始
return fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(postTitle, json))); // 再发出一个Action表示操作结束
};
};
// 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二
store.dispatch(fetchPosts('reactjs')).then(() =>
console.log(store.getState())
);
在上面代码中,fetchPosts
是一个Action Creator
,返回一个函数。然后再函数内部执行异步操作(fetch
),获取到异步操作结果后,再通过dispatch
发出Action
,更新store
中变量的值。
上面的代码中,有几点要注意:
fetchPosts
返回了一个函数,而普通的Action Creator
默认返回一个对象。- 返回的函数的参数是
dispatch
和getState
这两个Redux
方法,普通的Action Creator
的参数是Action
的内容。 - 在返回的函数之中,先发出一个
Action
表示操作开始。 - 异步操作结束之后,再发出一个
Action
表示操作结束。
我们知道,Action
是由store.dispatch
方法发送的。而store.dispatch
方法正常情况下,参数只能是对象,不能是函数。
因此,这时就要使用中间件redux-thunk
。
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';
// Note: this API requires redux@>=3.1.0
const store = createStore(
reducer,
applyMiddleware(thunk)
);
使用redux-thunk
中间件,改造store.dispatch
使得后者可以接受函数作为参数。
React-Redux的用法
React-Redux
将所有组件分成两大类:UI组件和容器组件。
UI组件
- 只负责UI的呈现,不带有任何业务逻辑
- 没有状态(即不使用
this.state
这个变量) - 所有数据都由参数(
this.props
)提供 - 不使用任何
Redux
的API
因为不含有状态,UI组件又称为“纯组件”,即它和纯函数一样,纯粹由参数决定它的值(参数相同返回的结果也相同)。
容器组件
- 负责管理数据和业务逻辑,不负责UI的呈现
- 带有内部状态
- 使用
Redux
的API
可以说:UI组件负责UI的呈现,容器组件负责管理数据和逻辑。
如果一个组件既有UI又有业务逻辑的话,我们的做法是,将其拆分成外面是一个容器组件,里面包着UI组件。前者负责与外部的通信,将数据传给后者,由后者渲染出视图。
React-Redux
规定,所有的UI组件都由用户提供,容器组件则是由React-Redux
自动生成,用户负责视觉层,状态管理则全部交给它。
connect()
React-Redux
提供connect
方法,用于从UI组件生成容器组件。其完整API如下:
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
connect
方法接受两个参数:mapStateToProps
和mapDispatchToProps
。它们定义了UI组件的业务逻辑:
mapStateToProps
负责输入逻辑,将state
映射到UI组件的参数(props
)mapDispatchToProps
负责输出逻辑,即将用户对UI组件的操作映射成Action
具体用法如下:
const mapStateToProps = (state) => ({
poemInfo: state.getIn(['poem', 'poemInfo']),
authorInfo: state.getIn(['poem', 'authorInfo']),
like: state.getIn(['poem', 'like']),
collect: state.getIn(['poem', 'collect']),
});
const mapDispatchToProps = (dispatch) => {
return {
getPoem(poem_id, category) {
return dispatch(getPoemInfo(poem_id, category)); // dispatch Action Creator
},
getAuthor(author_id, category) {
dispatch(getAuthorInfo(author_id, category));
},
getAudio(poem_id, category) {
return dispatch(getAudioInfo(poem_id, category));
},
getDynamic(poem_id, category) {
dispatch(getDynamicInfo(poem_id, category));
},
changeLikeStatus(status) {
dispatch(changeLike(status));
},
changeCollectStatus(status) {
dispatch(changeCollect(status));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(React.memo(Poem));
定义好mapStateToProps
将其作为参数传入connect
以后,在组件Poem
中,我们可以从props
获取poemInfo
、authorInfo
、like
、collect
数据,这就体现了输入逻辑。
与此同时,我们也可以在组件Poem
中从props
获取getPoem
、getAuthor
等方法,当组件内通过事件触发这些方法时,就可以通过dispatch
执行对应的Action
,改变store
中变量的值,从而进一步从props
中获取改变之后的变量值。
mapStateToProps
是一个函数,建立一个从(外部的)state
对象到(UI组件的)props
对象的映射关系。它返回一个对象,里面的每一个键值对就是一个映射。mapDispatchToProps
可以是函数也可以是对象,一般作为函数来使用,它返回一个对象,该对象的每个键值对都是一个映射,定义了每个方法对应发出怎样的Action
。(可以写成键值对的形式,也可以写成上面代码的形式)
Provider组件
connect
方法生成容器组件以后,需要让容器组件拿到state
对象,才能生成 UI 组件的参数。
一种解决方法是将state
对象作为参数,传入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级级将state
传下去就很麻烦。
React-Redux
提供Provider
组件,可以让容器组件拿到state
。
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
上面代码中,Provider
在根组件外面包了一层,这样一来,App
的所有子组件就默认都可以拿到state
了。它的原理是React
组件的context
属性。
React Redux实践
项目中想要使用React-Redux
一般遵循以下几个步骤:
安装项目依赖
npm install redux redux-thunk redux-immutable react-redux immutable --save
如果项目中使用到immutable.js
中的数据结构,则需要安装redux-immutable
,在合并不同模块的reducer
的时候需要用到redux-immutable
中的方法。
创建store
在src
目录或者app
目录下创建store
文件夹,并在其中新建index.js
和reducer.js
文件。
//reducer.js
import { combineReducers } from 'redux-immutable';
export default combineReducers ({
// 之后开发具体功能模块的时候添加 reducer
});
//index.js
import { createStore, compose, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore (reducer, composeEnhancers (
applyMiddleware (thunk)
));
export default store;
在项目中注入store
import React from 'react'
import { Provider } from 'react-redux'
import store from './store/index'
import routes from './routes/index.js'
function App () {
return (
<Provider store={store}>
...
</Provider>
)
}
export default App;
为需要使用redux的组件创建各自的store
// constants.ts
// 定义各种Action
export const GET_CURRENT_POEM = 'poem/GET_CURRENT_POEM';
export const GET_AUTHOR_INFO = 'poem/GET_AUTHOR_INFO';
export const GET_AUDIO_INFO = 'poem/GET_AUDIO_INFO';
export const GET_LIKE = 'poem/GET_LIKE';
export const GET_COLLECT = 'poem/GET_COLLECT';
// actionCreators.ts
// 定义各种Action和Action Creator
import {
GET_CURRENT_POEM,
GET_AUTHOR_INFO,
GET_AUDIO_INFO,
GET_LIKE,
GET_COLLECT,
} from './constants';
import { fromJS } from 'immutable';
import {
getPoemDetail,
getAuthorDetail,
getAudio,
getDynamic,
} from '@servers/servers';
export const changePoemInfo = (data) => ({
type: GET_CURRENT_POEM,
data: fromJS(data),
});
export const changeAuthorInfo = (data) => ({
type: GET_AUTHOR_INFO,
data: fromJS(data),
});
export const changeAudioInfo = (data) => ({
type: GET_AUDIO_INFO,
data: fromJS(data),
});
export const changeLike = (data) => ({
type: GET_LIKE,
data: fromJS(data),
});
export const changeCollect = (data) => ({
type: GET_COLLECT,
data: fromJS(data),
});
export const getPoemInfo = (poem_id, category) => {
return (dispatch) => {
return getPoemDetail(poem_id, category)
.then((res) => {
const curPoem = res[0];
if (category === '0') {
curPoem.dynasty = 'S';
} else if (category === '1') {
curPoem.dynasty = 'T';
}
dispatch(changePoemInfo(curPoem));
})
.catch((err) => {
console.error(err);
});
};
};
export const getAuthorInfo = (author_id, category) => {
return (dispatch) => {
if(author_id === undefined) {
dispatch(changeAuthorInfo({}));
}
getAuthorDetail(author_id, category)
.then((res) => {
if (res) {
dispatch(changeAuthorInfo(res[0]));
}
})
.catch((err) => {
console.error(err);
});
};
};
export const getAudioInfo = (poem_id, category) => {
return (dispatch) => {
return new Promise((resolve, reject) => {
getAudio(poem_id, category)
.then((data: any) => {
if (data.length > 0) {
dispatch(changeAudioInfo(data[0]));
resolve(true);
} else {
resolve(false);
}
})
.catch((err) => {
console.error(err);
});
});
};
};
export const getDynamicInfo = (poem_id, category) => {
return (dispatch) => {
getDynamic(poem_id, category)
.then((res: any) => {
const { like, collect } = res;
dispatch(changeLike(like));
dispatch(changeCollect(collect));
})
.catch((err) => {
console.error(err);
});
};
};
// reducer.ts
// 定义defaultState和reducer
import * as actionTypes from './constants';
import { fromJS } from 'immutable';
const defaultState = fromJS({
poemInfo: {},
authorInfo: {},
audioInfo: {},
like: false,
collect: false,
});
export default (state = defaultState, action) => {
switch (action.type) {
case actionTypes.GET_CURRENT_POEM:
return state.set('poemInfo', action.data);
case actionTypes.GET_AUTHOR_INFO:
return state.set('authorInfo', action.data);
case actionTypes.GET_AUDIO_INFO:
return state.set('audioInfo', action.data);
case actionTypes.GET_LIKE:
return state.set('like', action.data);
case actionTypes.GET_COLLECT:
return state.set('collect', action.data);
default:
return state;
}
};
// index.ts
// 将reducer、actionCreators export出去
import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as constants from './constants';
export { reducer, actionCreators, constants };
export
出去之后,要在全局的store
文件夹下的reducer
内导入每个组件的reducer
,将所有组件的reducer
进行合并,才能起到作用:
// store/reducer.ts
import { combineReducers } from 'redux-immutable';
import { reducer as searchReducer } from '@pages/Search/store/index';
import { reducer as playerReducer } from '@pages/Player/store/index';
import { reducer as poemReducer } from '@pages/Poem/store/index';
import { reducer as recordReducer } from '@pages/Record/store/index';
export default combineReducers({
search: searchReducer,
player: playerReducer,
poem: poemReducer,
record: recordReducer,
});
组件中使用
...
import { connect } from 'react-redux';
...
function Poem(props) {
...
const { poemInfo: poem, authorInfo: author, like, collect } = props; // 获取从mapStateToProps传入的外部(store)的state对象
const {
getPoem,
getAuthor,
getAudio,
getDynamic,
changeLikeStatus,
changeCollectStatus,
} = props; // 获取从mapDispatchToProps传入的方法
let poemInfo = poem ? poem.toJS() : {}; // 对象类型数据需要进行进一步的toJS操作
let authorInfo = author ? author.toJS() : {};
...
}
const mapStateToProps = (state) => ({
poemInfo: state.getIn(['poem', 'poemInfo']),
authorInfo: state.getIn(['poem', 'authorInfo']),
like: state.getIn(['poem', 'like']),
collect: state.getIn(['poem', 'collect']),
});
const mapDispatchToProps = (dispatch) => {
return {
getPoem(poem_id, category) {
return dispatch(getPoemInfo(poem_id, category));
},
getAuthor(author_id, category) {
dispatch(getAuthorInfo(author_id, category));
},
getAudio(poem_id, category) {
return dispatch(getAudioInfo(poem_id, category));
},
getDynamic(poem_id, category) {
dispatch(getDynamicInfo(poem_id, category));
},
changeLikeStatus(status) {
dispatch(changeLike(status));
},
changeCollectStatus(status) {
dispatch(changeCollect(status));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(React.memo(Poem));
遇到的复杂场景
之前遇到的场景是这样的:我要通过异步请求获取音频,这个音频想让它存储在全局store中的,所以要通过dispatch操作来发起请求。与此同时,我想在请求完成后,通过获取的数据是否为空来判断这个音频是否存在,存在的话跳转至播放器Player页面,不存在的话就弹出toast提示。
一开始想到的解决方案是:在store中存放一个status变量来表示数据是否为空,获取到数据就dispatch这个status为true。但可能因为dispatch是异步更新的原因,在组件中获取这个从props中传来的变量的值因延迟而有误,无法实现效果。
因为想要在请求完成后去判断数据是否为空,很自然会想到用.then。但如果不做任何改写,直接在方法后添加.then,会报错
store.dispatch(...).then is not a function
。
综上所述,问题可以归结为两点:1. 如何传递请求获取的数据为空这个信息;2. 如何解决store.dispatch(...).then的报错
先看第二个问题,有.then
的报错,说明这个方法它不是thenable
的,那什么是.thenable
的呢?很自然我们会想到Promise
。所以我们可以将原本的Action Creator
:
export const getAudioInfo = (poem_id, category) => {
return (dispatch) => {
getAudio(poem_id, category)
.then((data) => {
if (data.length > 0) {
dispatch(changeAudioStatus(true));
dispatch(changeAudioInfo(data[0]));
resolve(true);
} else {
dispatch(changeAudioStatus(false));
resolve(false);
}
})
.catch((err) => {
console.error(err);
});
};
};
改写成:
export const getAudioInfo = (poem_id, category) => {
return (dispatch) => {
return new Promise((resolve, reject) => {
getAudio(poem_id, category)
.then((data) => {
if (data.length > 0) {
dispatch(changeAudioStatus(true));
dispatch(changeAudioInfo(data[0]));
} else {
dispatch(changeAudioStatus(false));
}
})
.catch((err) => {
console.error(err);
});
});
};
};
然后在mapDispatchToProps
中,将dispatch(xxx)
改写成return dispatch(xxx)
:
const mapDispatchToProps = (dispatch) => {
return {
getAudio(poem_id, category) {
return dispatch(getAudioInfo(poem_id, category));
},
};
};
这么写完后,就不会报.then
的错误了。(参考:store.dispatch(...).then is not a function)
我们再来看第一个问题。我们现在不能够通过在store
中存储status
变量来传递数据是否存在的信息,因为dispatch
异步更新有延迟,在Promise
中,我们会使用resolve
和reject
来传递信息,可以在之后的.then
回调中获取这个信息。
所以,我们可以通过resolve
来传递数据是否存在的信息,存在则resolve(true)
,不存在则reject(false)
:
export const getAudioInfo = (poem_id, category) => {
return (dispatch) => {
return new Promise((resolve, reject) => {
getAudio(poem_id, category)
.then((data: any) => {
if (data.length > 0) {
dispatch(changeAudioInfo(data[0]));
resolve(true); // 数据存在
} else {
resolve(false); // 数据不存在
}
})
.catch((err) => {
console.error(err);
});
});
};
};
前端部分通过.then
回调传入的参数status
来判断是跳转页面还是弹出提示:
const handleListen = () => {
getAudio(id, category).then((status) => {
if (status) {
Taro.navigateTo({
url: '/pages/Player/index',
});
} else {
setShowToast(true);
}
});
};
总结
React-Redux
是React
的状态管理工具,它主要是为了解决在大型项目中,一些状态的共享问题。如果不使用React-Redux
,组件之间共享状态只能通过路由或者context
来实现,很麻烦。有了React-Redux
,一些会在多个组件中使用的变量就可以存储在store
中,组件内可以从store
中获取这个变量来使用,这么做代码整体结构更优雅,代码可读性也更好。