前言
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中获取这个变量来使用,这么做代码整体结构更优雅,代码可读性也更好。