目录
一. 为什么要使用Redux
二. Redux是什么
1. Redux的三大组成部分
2. Redux的工作流程
三. 怎么使用Redux
四. 异步Action
五. 中间件
需要导入的组件:
redux
react-redux
redux-thunk
-
redux-devtools
(可选):Redux开发者工具,支持热加载、action
重放、自定义UI等功能。 -
redux-persist
(可选):支持store
本地持久化。 -
redux-observable
(可选):实现可取消的action
。
yarn add redux
yarn add react-redux
yarn add redux-thunk
一. 为什么要使用Redux
我们知道每个组件都有它自己的state
,当state
发生变化时,该组件就会重新渲染。通常情况下,我们使用组件自己的state
就可以顺利完成开发了。但如果遇见如下情况:
- 某个组件的
state
,需要共享- 一个组件需要改变另一个组件的
state
- 某个
state
,需要在任何地方都能拿到——即它应该是一个全局state
- 一个组件需要改变某个全局
state
我们通过属性传值、回调函数传值、通知传值等通信方式也不是不能实现,只是会使得程序的数据流向越来越不清晰,模块与模块之间的耦合度越来越大,使用Redux可以很好的解决这两个问题。
二. Redux是什么
简单地说,Redux就是一个state
管理器,我们一般用它来管理一些全局state
或组件需要共享的state
,从而很方便地实现一些本来需要通过各种通信方式绕来绕去才能实现的业务功能,程序的数据流向变得非常清晰,模块之间的耦合度也大大降低。
1. Redux的三大组成部分
Redux由Store、Action、Reducer三部分组成,这三者相互勾连、协同完成了对应用State的管理(应用State,其实就是Redux内部提供的一个JS对象,我们的全局state
或组件需要共享的state
都需要放在它里面),其中Store是司令官,Action是命令,Reducer是执行者。
1.1 Store
store
是一个JS对象,我们可以把它看成是一个更大的容器,应用State就存放在它里面,它还提供了三个方法来进行应用State的读取、修改、发布操作,整个应用只能有一个store
。
-
createStore()
方法
Redux提供了createStore()
方法,用来生成一个store
,该方法接收一个reducer
函数作为参数。(store
创建完成后,它内部的应用State也就跟着创建成功了,只是没有值而已,应用State的初始值由reducer
函数提供)
import {createStore} from 'redux';
const store = createStore(reducer);
下面是createStore(reducer)
内部实现的伪代码,我们可以看看用它生成的store
内部结构是怎样的,这将有助于我们理解store
是什么。
const createStore = (reducer) => {
// store里存放着应用State
let state;
// store里存放着所有监听应用State变化的回调函数
let callbacks = [];
// 提供getState方法
const getState = () => state;
// 提供dispatch方法
const dispatch = (action) => {
// dispatch action的时候,dispatch方法内部会自动调用作为参数传进来的reducer函数
// 并把当前时刻的应用State和action作为参数传给reducer函数
// reducer函数执行后会返回一个全新的应用State
const newState = reducer(state, action);
// store更新应用State
state = newState;
// 一旦应用State发生变化,就触发所有的回调函数
callbacks.forEach(callback => callback());
};
// 提供subscribe方法
const subscribe = (callback) => {
callbacks.push(callback);
return () => {
// 过滤掉重复的回调函数
callbacks = callbacks.filter(cb => cb !== callback);
}
};
// 可见store仅仅对外暴露了三个方法,应用State并不会直接暴露出去,因此我们无论是读取、修改还是发布应用State,都是通过这三个方法来完成
return {getState, dispatch, subscribe};
};
-
getState()
方法
store
提供了getState()
方法,用来读取当前时刻的应用State。
import {createStore} from 'redux';
const store = createStore(reducer);
const state = store.getState();
-
dispatch(action)
方法
store
提供了dispatch(action)
方法,用来发出一个action
,目的是修改应用State。我们可以在任意的地方调用store.dispatch(action)
,组件中、网络请求的回调中、定时器中都可以。
import {createStore} from 'redux';
const store = createStore(reducer);
store.dispatch(action);
-
subscribe(callback)
方法
store
提供了subscribe(callback)
方法,用来发布最新的应用State,组件设置对应用State的监听,并设置回调函数,一旦应用State发生变化,store
就会自动触发这个回调函数。
import {createStore} from 'redux';
const store = createStore(reducer);
store.subscribe(callback);
当我们调用store.subscribe(callback)
方法时,该方法会返回一个函数,我们调用这个返回的函数就可以解除组件对应用State的监听。
let unsubscribe = store.subscribe(callback);
unsubscribe();
1.2 Action、Action Creator
(1)Action
上面Store部分,我们说到store.dispatch(action)
方法是修改应用State的唯一方式,可以看到该方法发出了一个action
,那这个action
是什么呢?
action
是一个JS对象,这个JS对象必须得有一个type
属性,是命令的名字,起名字时要做到见名知意,其它的属性我们可以自由设置,用来携带命令的负载信息。一个action
,它的名字描述了应用State要做什么样的变化,它的负载信息则是提供了应用State做这个变化所需要的原料(reducer
是接触不到组件的,但是action
可以,所以如果reducer
需要某些组件传进来的数据,就只能通过action
负载信息给带进来)。
const action = {
type: ...,
pros1: ...,
pros2: ...,
...
};
举个实际例子,下面就定义了一个action
,执行者在收到这个命令后,一看到命令的名字是ADD
,它就知道是要对应用State做加法操作,再一看命令的负载信息,它就得到了应用State要加1
。
const action = {
type: 'ADD',
payload: 1,
};
多数情况下,type
会被定义成字符串常量,放在单独的actionTypes.js
文件里,方便我们管理和使用。而当应用规模越来越大时,action
也会越来越多,也建议放在单独的rootAction.js
文件里,方便我们管理和使用。
// actionTypes.js
export default {
ADD: 'ADD',
}
// rootAction.js
import Type from '../type';
const action = {
type: Type.ADD,
payload: 1,
};
(2)Action Creator
我们的项目中,应用State值的变化肯定会很多,那就要需要创建很多action
,如果我们一个一个写action
,那烦都要烦死了,因此我们可以定义一个函数来专门生成action
,这个的函数就是Action Creator。
例如,我们现在要实现加2
,加3
的操作,如果没有Action Creator,则要再写两个action
。
const action1 = {
type: 'ADD',
payload: 1,
};
const action2 = {
type: 'ADD',
payload: 2,
};
const action3 = {
type: 'ADD',
payload: 3,
};
但是如果编写一个Action Creator,将会是这样。
function add(payload) {
// action就是JS对象,我们返回一个JS对象
return {
type: 'ADD',
payload,
}
}
const action1 = add(1);
const action2 = add(2);
const action3 = add(3);
上面的add()
函数就是一个Action Creator,可见有了它的确方便了不少。
1.3 Reducer、Reducer的拆分、Reducer的合并
(1)Reducer
上面Store部分,我们说到在使用createStore(reducer)
方法创建一个store
时,该方法会接收一个reducer
函数作为参数,并且在store.dispatch(action)
时会自动调用这个reducer
函数,那这个reducer
是什么呢?
reducer
是一个函数,它接受当前时刻的应用State和action
作为参数,在函数执行体里完成应用State的修改(利用原State的数据和action
的负载信息完成修改,当然如果reducer
内部本身就知道怎么修改应用State,也可以直接修改,不用action
的负载信息),生成一个全新的应用State并返回(注意一定要生成一个全新的应用State,而不是修改原应用State后返回)。也就是说,reducer
函数描述了应用State的具体修改过程。
const reducer = (state, action) => {
// 根据原State和action,修改应用State,生成一个全新的应用State
const newState = ...
// 返回全新的应用State
return newState;
}
通常情况下,我们还会在创建reducer
的时候,把应用State的初始值也设置好。
const defaultState = {number: 0};
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD':
return {
...state,
number: state.number + action.payload,
};
case 'SUB':
return {
...state,
number: state.number - action.payload,
};
}
}
(2)Reducer的拆分
由于整个应用只有一个State,那对于大型应用来说,这个应用State必然十分庞大,而应用State修改过程是在reducer
函数体内进行的,所以如果应用State的修改多,那这个reducer
函数也将十分庞大。请看下面的例子。
const defaultState = {
isLogin: true,
friends: ['张三', '李四'],
tabbarThemeColor: 'red',
};
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'CHANGE_LOGIN_STATUS':
return {
...state,
isLogin: action.payload,
};
case 'ADD_FRIEND':
return {
...state,
friends: state.friends.push(action.payload),
};
case 'CHANGE_TABBAR_THEME_COLOR':
return {
...state,
tabbarThemeColor: action.payload,
};
default:
return state;
}
}
上面代码中,isLogin
、friends
和tabbarThemeColor
三个东西的状态没有什么联系,可以理解为它们属于三个相互独立的模块,它们修改应用State其实也是相互独立的,因此为了避免reducer
过于庞大,我们在开发中通常会把reducer
拆分成多个子reducer
,放在多个.js
文件里,一个reducer
专门负责修改一个东西。
-----------isLoginReducer.js-----------
const defaultState = {
isLogin: true,
};
const isLoginReducer = (state = defaultState, action = {}) => {
const {type, payload} = action;
switch (type) {
case 'CHANGE_LOGIN_STATUS':
return {
...state,
isLogin: action.payload,
};
default:
return state;
}
}
-----------friendsReducer.js-----------
const defaultState = {
friends: ['张三', '李四'],
};
const friendsReducer = (state = defaultState, action = {}) => {
const {type, payload} = action;
switch (type) {
case 'ADD_FRIEND':
return {
...state,
friends: state.friends.push(action.payload),
};
default:
return state;
}
}
-----------tabbarThemeColorReducer.js-----------
const defaultState = {
tabbarThemeColor: 'red',
};
const tabbarThemeColorReducer = (state = defaultState, action = {}) => {
const {type, payload} = action;
switch (type) {
case 'CHANGE_TABBAR_THEME_COLOR':
return {
...state,
tabbarThemeColor: action.payload,
};
default:
return state;
}
}
(3)Reducer的合并
上面我们已经成功把一个庞大的reducer
拆分成了若干个子reducer
,但是我们知道Redux在创建store
的时候,createStore(reducer)
方法只能接收一个reducer
,也就是说store
只认一个reducer
,所以我们还得把这若干个子reducer
合并成一个大reducer
供创建store
时使用。
Redux提供了combineReducers()
方法把若干个子reducer
合并成一个根reducer
,该函数接收一个JS对象为参数,该JS对象其实就是应用State,这是根reducer
函数在为应用State赋值,所有的子reducer
在修改它负责的那个模块的state
后都会来这里重新给应用State赋值。为了方便理解,我们给应用State里的key
起名字时都起作xxxState
,而value
必须是某个对应的子reducer
函数,它可以返回一个state
,所以这一对key-value
就专门负责描述xxx
的state
。
// rootReducer.js
const reducer = combineReducers({// 该JS对象其实就是应用State
// 应用State的属性名:该属性对应的reducer
isLoginState: isLoginReducer,
tabbarThemeColorState: tabbarThemeColorReducer,
friendsState: friendsReducer,
});
不过虽然说是合并,但合并之后和合并之前的应用State的数据结构是发生变化了的,这从应用State的默认值可以看得出来。比如,合并之前应用State的默认值为
const defaultState = {
isLogin: true,
friends: ['张三', '李四'],
tabbarThemeColor: 'red',
};
而合并之后应用State的默认值为
const defaultState = {
isLoginState: {isLogin: true},
friendsState: {friends: ['李四', '王五']},
tabbarThemeColorState: {tabbarThemeColor: 'red'},
};
此时,你可能又会问,合并之前很好理解,dispatch(action)
方法里会自动找到那个大的reducer()
,进去一判断就知道要怎么修改应用State,那合并之后呢?我们dispatch
一个dispatch
,dispatch
方法里是怎么找对应的子reducer()
的呢?我暂时也不确认,但我打断点看了一下,修改任意一个属性,确实会走所有的子reducer
,也许合并后的reducer
函数还是合并前那样?
2. Redux的工作流程
到了这里,我们就可以对Redux的工作流程做一下梳理了。
组件和Action环节:点击组件,构建
action
(action
需携带有效的负载信息),调用store
的dispatch(action)
方法把这个action
给发出去。Store环节:
store
在dispatch(action)
出一个action
之后,会立即自动触发它创建时接收的那个reducer
函数,并把当前时刻的应用State和action
作为参数传给reducer
。Reducer环节:
reducer
函数在接收到应用State和action
两个参数后,会根据原应用State和action
的payload
完成对应用State的改变,然后返回新的应用State给store
。Store环节:
store
接收到新应用State后,就会更新应用State,而应用State一旦发生改变,就会立即触发监听了应用State变化的组件的回调函数。组件环节:回调函数触发后,我们可以在回调函数里做一些自定义的处理。
三. 怎么使用Redux
抓住store
、action
、reducer
这三个关键词,然后按下面的流程来就可以了。我们先来看一个最简单的计数器实例,后面的文章中会有复杂一点的例子。
// TestPage.js
/**
* 使用Redux写的计数器,目的只是为了练习一下Redux使用方法。
* 你可以尝试用this.state写一下计数器,对比一下,肯定会觉得使用Redux反而更麻烦了,是的,因为这个例子太简单了,而Redux有它专门的适用场景。
*/
import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View, Button} from 'react-native';
// 导入Redux的相关东西
import {createStore, combineReducers} from 'redux';
// 第2步:
// 创建根reducer,合并所有子reducer
// 刚创建根reducer时,我们可能不知道将来会有那些组件的state会被放在应用state里来统一管理,所以可以先空着,什么时候需要什么时候往这里添
// 计数器的初始state
const defaultState = {
number: 100,
}
// 编写子reducer,负责计数器state具体变化的过程
const counterReducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD_NUMBER':
return {
...state,
number: state.number + 1
};
default:
return state;
}
}
const rootReducer = combineReducers({// 这个对象就是应用State
// 应用State赋值
counterState: counterReducer,
});
// 接下来第3步,就是结合该组件reducer里action.type的规定,为该组件创建对应的action,预备好action,到时候组件一被触摸就dispatch一个action
// 第1步:
// 创建项目唯一的store,此时应用State也跟着创建好了
// 发现需要一个reducer,所以接下来第2步,我们去创建一个reducer,回过头来填在这里
const store = createStore(rootReducer);
// 第3步:
// 为该组件创建对应的action,预备好action,到时候组件一被触摸就dispatch一个action
// 负责描述state要做什么变化以及变化所需的原料,用来dispatch
const addNumberAction = {type: 'ADD_NUMBER'};
// 编写UI组件
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
number: store.getState().counterState.number,
};
}
render() {
return (
<View style={styles.counterViewStyle}>
<Text style={{fontSize: 24}}>{this.state.number}</Text>
<View>
<Text
style={{color: 'black', fontSize: 20}}
// 第5步:点击组件的时候发出一个action
onPress={() => store.dispatch(addNumberAction)}
>{'+'}</Text>
</View>
</View>
);
};
componentDidMount() {
// 第4步:设置监听
store.subscribe(() => {
this.setState({
number: store.getState().counterState.number,
});
});
}
}
// 导出组件
export default class TestPage extends Component {
render() {
return (
<View style={styles.container}>
<Counter/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
counterViewStyle: {
backgroundColor: 'pink',
width: 200,
height: 60,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
},
});
四. 异步Action
上面的情况都是在同步操作下,我们修改应用State很简单,只需要在想要修改的地方dispatch()
一个action
就可以了。
但是在异步操作下(例如网络数据请求、数据库数据加载等),我们通常需要dispatch()
至少两个action
,即:
- 异步操作开始时,需要发出一个
action
,触发state
更新为“正在操作”状态,组件重新渲染。 - 异步操作结束后,需要再发出一个
action
,触发state
更新为“操作结束”状态,组件再一次重新渲染。
我们假设有一个网络请求,将会做如下编写:
export function fetchFriends(url) {
// 异步操作开始前,发送一个action
dispatch({type: 'FETCH_FRIENDS'});
// 异步操作
fetch(url)
.then(response.json())
.then(json => {
// 异步操作结束后,再发送一个action
dispatch({type: 'RECEIVE_FRIENDS', payload: json});
});
}
但是我们现在要用Redux来管理应用State,所以就不能直接触发这个函数执行数据请求,而是发出一个action
来执行,因此会改写如下:
export function fetchFriends(url) {
// 可见fetchFriends的返回值为一个箭头函数
return dispatch => {
// 异步操作开始前,发出一个action
dispatch({type: 'FETCH_FRIENDS'});
// 异步操作
fetch(url)
.then(response.json())
.then(json => {
// 异步操作结束后,再发出一个action
dispatch({type: 'RECEIVE_FRIENDS', payload: json});
});
}
}
当编写了fetchFriends()
函数后,触发组件开始请求数据,此时就需要dispatch()
一下,而dispatch()
的内容正是fetchFriends()
函数,即:
store.dispatch(fetchFriends());
现在有点蒙,fetchFriends
函数执行后返回的值明明是一个函数,怎么可以作为store.dispatch()
方法的参数呢?store.dispatch()
方法不是只能接收一个action
吗?而action
不是只能是一个JS对象吗?
对了一半错了一半,通常情况下action
只能是一个JS对象,但其实它可以是随便一个东西,当然也就可以是一个函数。没错,store.dispatch()
方法是只能接收一个JS对象的action
,但我们使用redux-thunk中间件的作用就是把store.dispatch()
方法改造得可以接收函数了。
于是,上面的fetchFriends
方法本身就成了一个Action Creator
,而它执行后返回的函数就是一个action
,我们把这样的action
称为异步action
,因为在它体内执行了一个异步操作。
简单一句话,同步action
是一个JS对象,而异步action
是一个体内执行了某个异步操作的函数。
五. 中间件
1. 什么是中间件
为了理解中间件,让我们站在框架作者的角度思考问题:在使用Redux的过程中,如果我们想要添加一个功能,会添加在哪个环节?
- 组件环节:与State一一对应,可以看作State的视觉层,不适合承担其它操作。
- Action环节:Action仅仅是描述State如何变化,及变化所需原料的JS对象,也不适合承担其它操作。
- Reducer环节:Reducer是一个纯函数,它的内部不应该做其它任何多余的操作,而只应负责负责了State的具体变化过程,并返回一个全新的State。
既然这几个环节都不适合的话,就剩下一个环节了呀——Store环节。而不是getState()
和subscribe()
方法的功能本身就很明确,发挥空间不大,所以看来这个功能就只能添加在store.dispatch(action)
的时候。
比如说我们要添加打印action
和state
的功能,则会改造store
的dispatch(action)
方法如下。
// 获取store原来的dispatch方法
let originalDispatch = store.dispatch;
// 编写中间件,dispatchAndLog函数就是中间件
function dispatchAndLog(action) {
// 自定义实现
console.log('dispatching', action);
// 调用一下dispatch方法的原生实现
originalDispatch(action);
// 自定义实现
console.log('new state', store.getState());
};
// 改造store原来的dispatch方法
store.dispatch = dispatchAndLog;
这样我们就得出结论:中间件其实就是一个函数,它就是对dispatch
方法进行了改造,以便我们添加一些自定义的操作。
2. 怎么使用中间件
至于我们为什么要使用中间件,就不多说了,因为使用Redux的时候,我们可能要添加一些自定义的操作,就得使用中间件。
同时我们也不会涉及如何编写中间件,因为常用的中间件别人都写好了,我们只需要导入使用即可。
下面仅举个例子,看下如何使用中间件。
// 导入Redux提供的应用中间件的方法:applyMiddleware
import {createStore, applyMiddleware} from 'redux';
// 导入需要使用的中间件组件
import thunk from 'redux-thunk';
import createLogger from 'redux-logger';
// 创建中间件
const logger = createLogger();
let middlewares = [
thunk,
logger
];
const store = createStore(reducer, applyMiddleware(...middlewares));
可见中间件的使用也非常简单,只需要:
- 导入你需要的中间件或创建中间件。
- 然后在使用
createStore
方法创建store
的时候,第二个参数使用applyMiddleware()
方法应用一下这个中间件就可以了。
我们把中间件apply
到那个store
,Redux会在applyMiddleware()
方法里自动完成该store
的dispatch
方法的改造,我们不必去关心。不过要注意在使用applyMiddleware()
方法时,中间件的顺序是有要求的,使用前要查一下文档,比如logger
就一定要放在最后,否则输出结果会不正确。
参考博客:
阮一峰:Redux入门教程——基本用法
阮一峰:Redux入门教程——中间件与异步操作