Redux是用来管理JavaScript应用的状态的。官方文档地址:Redux。本文讲述如何在React中使用Redux,重点偏向Redux。如果需要了解React,可以参考React官网或者React入门(前一阵入门React时写的文|ω・))。
Redux简介
-
Redux是什么?
Redux is a predictable state container for JavaScript apps.
Redux是一个用在JavaScript应用中的可预测状态容器(这里的状态指的是存放信息的对象)。Redux是用来对状态进行统一管理的,整个应用的状态以一棵对象树的方式被存放在store中。状态是只读的,唯一改变状态的方式是dispatch actions,所以状态的改变是可预测的。通过dispatch改变store中的state,通过subscribe监听状态的改变,通过getState来拿到当前的状态,这样数据的处理都集中在Redux中。
-
为什么要用Redux?
想象这样的场景,组件1、组件2、组件3、组件5、组件7(下图标绿的组件)中都需要使用一些数据,因为在React中,数据是单向流动的,所以在组件7中改变数据了,就需要组件7通知组件4改变数据,组件4再通知组件1改变数据。在组件1中改变数据之后再层层往下传递数据。这还只是下图这样结构比较简单的应用,如果应用更复杂,那处理起来就是一团乱麻了。所以我们需要用Redux来对数据(状态)进行统一的管理,当需要改变某数据时,直接改变store中的数据,当要获取数据时,也直接从store中拿数据。
图-1 没使用Redux时的处理流程图-2 使用Redux时的处理流程 -
React和Redux有什么关系?
听见Redux这个名字的时候,因为和React一样都是以Re开头的,可能会产生Redux是专门为React解决问题的一个工具这样的误解,但实际上React和Redux啥关系也没有,是两个独立的工具。你可以将Redux和React结合起来使用,也可以将Redux和jQuery,甚至Redux和JavaScript结合使用。
Redux
Redux中有几个必须知道的概念:action、reducer、store。下文中提到的state(状态)指的是存放信息的对象,这个对象是放在Redux的store中的。
actions
actions是从应用到store的数据的载体。当我们需要改变store中的数据的时候,需要dispatch actions。action必须有一个type属性,表明即将执行的action的类型,type通常被定义为一个字符常量。action对象中的其他属性是携带的信息。
例如:
{
type: ADD_NUMBER,
number: 1
}
action creator是创建action的函数,简单地返回action,它能根据传入的参数设置action的属性值。
例如:
const ADD_NUMBER = 'ADD_NUMBER'
export const addNumber = number => ({
type: ADD_NUMBER,
number
})
reducers
reducer是纯函数,它是用来指明当action发送(dispatch)到store的时候,state是如何改变的。它的第一个参数为前一个state,第二个参数为action对象,返回的值是下一个state。
纯函数:
- 传入相同的参数会返回相同的结果。
- 执行纯函数不会造成副作用。(这里的副作用指的是函数改变了作用域之外的状态值,比如函数外部定义了变量a,在函数内部改变了变量a的值。)
在reducer中不能做以下操作:改变它的参数;造成副作用;调用非纯函数(比如Math.random()
)。
以下的caculate函数是一个简单的reducer。如果createStore函数没有传入第二个参数(state的初始值),那么首次调用caculate的时候,实参state的值为undefined。所以给state参数设置了一个默认值0。
function caculate (state = 0, action) {
const { number } = action
switch (action.type) {
case 'ADD_NUMBER':
return state + number
case 'SUBTRACT_NUMBER':
return state - number
case 'MULTIPLY_NUMBER':
return state * number
default:
return state
}
}
当应用比较大的时候,一般的做法是使用多个小的reducers,每个reducer处理特定的内容。然后使用Redux提供的combineReducers
将reducers合并为一个reducer,合并后的reducer作为createStore的参数来创建store实例。下文介绍完store之后,会给出一个简单的例子来说明这部分内容。
store
store将actions和reducers结合起来,store的功能是:
- 保存应用的state。
- 通过
getState()
拿到state。 - 通过
dispatch(action)
更新state。 - 通过
subscribe(listener)
注册监听器。listener是一个函数,当发送action的时候会执行listener。 - 执行
subscribe(listener)
会返回 一个函数,调用这个函数能够注销监听器。
一个应用中只能有一个store,想要拆分数据的话,需要使用多个reducers。
以下代码中的caculate是上文创建好的reducer,addNumber是上文定义的action creator。
import { createStore } from 'redux'
const store = createStore(caculate)
const listener = () => console.log(store.getState()) // 打印当前状态
const unsubscribe = store.subscribe(listener)
store.dispatch(addNumber(1))
store.dispatch(addNumber(2))
store.dispatch(addNumber(3))
unsubscribe()
使用store.subscribe(listener)注册了监听器,当store.dispatch()发送action到store,reducer将state改变之后,监听器函数listener会执行,打印出以下结果:
1
3
6
我需要另一个reducer来处理标签切换,以下是相应的action creatore和reducer。
const SELECT_TAB = 'SELECT_TAB'
const selectTab = tab => ({
type: SELECT_TAB,
tab
})
function switchTab (state = 'CACULATE', action) {
switch (action.type) {
case 'SELECT_TAB':
return action.tab
default:
return state
}
}
现在就有了两个reducer。使用combineReducers将reducers合并为一个根reducer,将根reducer作为createStore的第一个参数,初始的state值作为createStore的第二个参数来创建store。
let state = {
caculate: 0,
switchTab: 'CACULATE'
}
const reducer = combineReducers({ caculate, switchTab })
const store = createStore(reducer, state)
const listener = () => console.log(store.getState())
const unsubscribe = store.subscribe(listener)
store.dispatch(addNumber(1))
store.dispatch(addNumber(2))
store.dispatch(addNumber(3))
unsubscribe()
combineReducers所做的事情是生成一个函数,调用这个函数时,会执行每个reducer,并将执行的结果重新整合为一个对象,作为新的state。当调用store.dispatch(addNumber(1))
时,两个reducer(caculate和switchTab)都会被调用,它们执行的结果会组成一个新的state树。
代码段中的:
const reducer = combineReducers({ caculate, switchTab })
和以下代码段的效果是一样的,
function reducer (state = {}, action) {
return {
caculate: caculate(state.caculate, action),
switchTab: switchTab(state.switchTab, action)
}
}
只是combineReducers会有更多的处理操作。
在React项目中使用Redux
react-redux插件是专门用于在React中使用redux的,react-redux官网地址。
React Redux将展示组件(presentational components)和容器组件(container components)分离了。
Redux官网中有两者的对比表格:
Presentational Components | Container Components | |
---|---|---|
Purpose | How things look (markup, styles) | How things work (data fetching, state updates) |
Aware of Redux | No | Yes |
To read data | Read data from props | Subscribe to Redux state |
To change data | Invoke callbacks from props | Dispatch Redux actions |
Are written | By hand | Usually generated by React Redux |
容器组件能将展示组件和Redux联系起来。一般使用React Redux提供的connect方法生成容器组件。
React Redux
-
connect 方法
调用connect方法会返回一个包装函数,这个函数接收组件并返回一个新的包装组件。这里的包装组件即是上文说到的容器组件,容器组件能将展示组件和Redux联系起来。
connect的使用方法是:
connect(mapStateToProps? mapDispatchToProps?, mergeProps?, options?)(组件)
- mapStateToProps,这个参数是一个函数,当store更新的时候,mapStateToProps就会被调用,如果不想订阅store的更新,就在这个位置传null或undefined。从函数名可知,这个参数的作用是将Redux store的state映射到React组件的属性。mapStateToProps函数的格式是这样的:(state, ownProps?) => Object,state指Redux store中的state,第二个参数是组件自己的属性。这个函数执行后返回的对象会和组件的属性合并。
- mapDispatchToProps,这个参数可以是一个对象或者函数。从函数名可知,这个参数的作用是将Redux store的dispatch映射到React组件的属性,如果没有传入这个参数,那么组件会默认收到dispatch属性,这个dispatch属性的值是Redux store的dispatch方法。如果传入的参数是一个函数,那么这个函数是这样的:(dispatch, ownProps?) => Object,它的第一个参数是Redux中的dispatch,第二个参数是组件自己的属性。这个函数返回一个对象,这个对象的每个属性值都是一个函数,这个对象会合并到组件的属性中。
-
Provider组件
所有的容器组件都需要能够访问Redux的store,如果不使用Provider组件,就只能在所有容器组件的属性中传入store,假如在组件树中深层嵌套了容器组件,可能有的展示组件也需要传入store属性。但是使用Provider组件包裹上应用根组件后,应用中的所有容器组件就都能访问到Redux的store了。
举个例子,定义容器组件的时候这样写:
import { connect } from 'react-redux'
import { addNumber, subtractNumber, multiplyNumber } from '../actions'
import Caculation from '../components/Caculation'
const mapStateToProps = state => ({
caculate: state.caculate
})
const mapDispatchToProps = dispatch => ({
plus: number => dispatch(addNumber(number)),
subtract: number => dispatch(subtractNumber(number)),
multiply: number => dispatch(multiplyNumber(number))
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Caculation)
那么展示组件Caculation的属性中就会有caculate,plus,subtract,multiply这几个属性,其中caculate是state.caculate;plus,subtract,multiply是三个函数,调用函数的时候能进行dispatch操作。
异步获取信息
Redux只支持同步数据流,如果想要使用异步actions,需要使用applyMiddleware增强createStore。使用redux-thunk中间件能够发起同步或异步的actions,并且可以设计满足某种条件才dispatch actions。使用Redux Thunk之后,action creator可以返回一个函数,这个函数的第一个参数是dispatch,第二个参数是getState。 首先下载好redux-thunk,并使用applyMiddleware增强createStore:
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
...
const store = createStore(rootReducer, applyMiddleware(thunk))
在thunk中间件的帮助下,就可以像下面这样使用异步action 了。在请求开始和请求结束的地方都触发一个action,在请求开始的地方触发的action可以用来展示一个加载中的loading图等。这里只简单地在请求开始时触发了action,没有做后续的使用。
const requestLeaderBoard = () => ({
type: REQUEST_LEADERBOARD
})
const receiveLeaderBoard = (json) => ({
type: RECEIVE_LEADERBOARD,
leaderboard: json.data
})
export function fetchLeaderBoard () {
return dispatch => {
dispatch(requestLeaderBoard())
return fetch(`这里用一个get请求的地址`)
.then(res => res.json())
.then(json => dispatch(receiveLeaderBoard(json)))
}
}
为了学习redux写了一个练习项目:源码地址
遇到的问题
-
定义的reducer执行了“多余”的次数。
- 情况1:没有使用combineReducers时,reducer会多执行1次,action的type为:
@@redux/INITd.c.p.m.e.7
。 - 情况2:使用combineReducers之后,每个reducer都多执行了3次,action的type分别是:
@@redux/INITf.1.c.7.u.x
、@@redux/PROBE_UNKNOWN_ACTIONz.n.g.2.k.9
、@@redux/INITf.1.c.7.u.x
。有3个多出的reducer调用。
情况2的问题请教了可爱的同事杨老板,杨老板找到了相应的源码给出了解答,然后我又按照相同的方式在源码中找了下,得到了情况1的答案。
(1)情况1出现的原因:
当使用createStore创建好一个store之后,Redux会dispatch一个初始化action(如下),以保证每个reducer返回它们的默认state。
dispatch({ type: ActionTypes.INIT })
(2)情况2出现的原因:
执行第一个action
@@redux/INITf.1.c.7.u.x
是因为Redux在执行combineReducers的过程中调用了一遍reducer,作用是检查调用reducer返回的初始state是不是undefined,如果是undefined会抛出一个错误。const initialState = reducer(undefined, { type: ActionTypes.INIT })
第二个action
@@redux/PROBE_UNKNOWN_ACTIONz.n.g.2.k.9
也是在combineReducers执行的过程中进行处理的,目的是检查action的名称和Redux内私有的名称是否重复,如果重复,就抛出一个错误。第三个action和情况1同。
- 情况1:没有使用combineReducers时,reducer会多执行1次,action的type为: