随着单页应用变得越来越复杂,前端代码需要管理各种各样的状态,它可以是服务器的响应,也可能是前端界面的状态。当这个状态变得任意可变,那么你就可能在某个时间点失去对整个应用状态的控制。
Redux 就是为了解决这个问题而诞生的。
简短地说,Redux 为整个应用创建并管理一棵状态树,并通过限制更新发生的时间和方式,而使得整个应用状态的变化变得可以被预测。除此之外,Redux 有着一整套丰富的生态圈,包括教程、中间件、开发者工具及文档,这些都可以在官方文档中找到。
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。
Redux是将整个应用状态存储到一个地方上称为store,里面保存着一个状态树store tree,组件可以派发(dispatch)行为(action)给store,而不是直接通知其他组件,组件内部通过订阅store中的状态state来刷新自己的视图。
npm install redux --save
多数情况下,我们还需要使用 React 绑定库和开发者工具。
npm install redux-devtools --save-dev
在使用 Redux 之前,你必须要谨记它的三大原则:单一数据源、
state
是只读的和使用纯函数执行修改。
1.单一数据源
整个应用的 state
都被储存在一棵树中,并且这棵状态树只存在于唯一一个 store
中。
这使得来自服务端的 state
可以轻易地注入到客户端中;并且,由于是单一的 state
树,代码调试、以及“撤销/重做”这类功能 的 实现也变得轻而易举。
2.只读的 state
唯一一改变 state
的方法就是触发 action
,action
是一个用于描述已发生事件的普通对象。
这就表示无论是用户操作或是请求数据都不能直接修改 state
,相反它们只能通过触发 action
来变更当前应用状态。其次,action
就是普通对象,因此它们可以被日志打印、序列化、储存,以及用于调试或测试的后期回放。
3.使用纯函数执行修改
为每个 action
用纯函数编写 reducer
来描述如何修改 state
树
store.dispatch()
将 action 传到 store。redux约定 Action 内使用一个字符串类型的type字段来表示将要执行的动作名称。
{
type: 'INCREASE'
}
除了type
之外,Action还可以携带需要的数据。
{
type: 'INCREASE',
text: 'Learn Redux'
}
View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator。
function increase(text) {
return {
type: INCREASE,
text:text
}
}
const action = addTodo('Learn Redux')
上面代码中,increase函数就是一个 Action Creator,在 Redux 中的 Action Creator 只是简单的返回一个 action而已
在Redux 中只需把 Action Creator的结果传给 dispatch() 方法即可发起一次 dispatch 过程。
dispatch(increase(text))
注意: store
里能直接通过 store.dispatch()
调用dispatch()
方法
const defaultState = 10
const reducer = (state = defaultState, action) => {
switch (action.type) {
case Constants.INCREASE:
return state + 1
case Constants.DECREASE:
return state - 1
default:
return state
}
}
const state = reducer(10, {
type: Constants.INCREASE
})
上面代码中,reducer函数收到名为INCREASE的 Action 后,就返回一个新的 State,作为加法的计算结果。
实际开发中,Reducer
函数不用像上面这样手动去调用,store.dispatch
方法会触发 Reducer
的自动调用,为此,Store
需要知道 Reducer
函数,做法就是在生成 Store
的时候,将 Reducer
传入到createStore
函数中
import { createStore } from 'redux'
import reducer from '../reducer'
// 创建store
const store = createStore(reducer)
上面代码中,createStore
接受 Reducer
作为参数,生成一个新的 Store
。这样以后每当store.dispatch
发送过来一个新的 Action
,就会自动调用 Reducer
,得到新的 State
真正开发项目的时候State
会涉及很多功能,在一个Reducer
函数中处理所有逻辑会非常混乱,所以需要拆分成多个子Reducer
,每个子Reducer
只处理它管理的那部分State
数据。然后在由一个主rootReducers
来专门管理这些子Reducer
。
Redux
提供了一个方法:combineReducers专门来管理这些子Reducer
import {createStore, combineReducers} from 'redux'
const list = (state = [], action) => {
switch (action.type) {
case ADD_ITEM:
return [createItem(action.text), ...state]
default:
return state
}
}
const = (state = defaultState, action) => {
switch (action.type) {
case Constants.INCREASE:let nextState = todoApp(previousState, action)
return state + 1
case Constants.DECREASE:
return state - 1
default:
return state
}
}
let rootReducers = combineReducers({list, counter})
注意: 当使用combination
的时候,combination
会把所有子Reducer
都执行一遍,子Reducer
通过action.type
匹配操作,因为是执行所有子Reducer
,所以如果两个子Reducer
匹配的action.type
是一样的,那么都会匹配成功。
reducer
组合 而不是创建多个 store
。reducer
来创建 store
是非常容易的。在前一个章节中,我们使用 combineReducers()
将多个 reducer
合并成为一个。现在我们将其导入,并传递给createStore
函数。import { createStore } from 'redux'
import reducer from '../reducer'
const store = createStore(reducer)
Store提供暴露出四个API方法
import { createStore } from 'redux'
let { subscribe, dispatch, getState, replaceReducer} = createStore(reducer)
下面是createStore方法的一个简单实现,可以了解一下 Store 是怎么生成的。
let createStore = (reducer) => {
let state;
//获取状态对象
//存放所有的监听函数
let listeners = [];
let getState = () => state;
//提供一个方法供外部调用派发action
let dispath = (action) => {
//调用管理员reducer得到新的state
state = reducer(state, action);
//执行所有的监听函数
listeners.forEach((l) => l())
}
//订阅状态变化事件,当状态改变发生之后执行监听函数
let subscribe = (listener) => {
listeners.push(listener);
}
dispath();
return {
getState,
dispath,
subscribe
}
}
let combineReducers=(renducers)=>{
//传入一个renducers管理组,返回的是一个renducer
return function(state={},action={}){
let newState={};
for(var attr in renducers){
newState[attr]=renducers[attr](state[attr],action)
}
return newState;
}
}
export {createStore,combineReducers};
其实简单来说Redux就是个发布订阅系统。
Redux工作的流程图
1.首先,用户发出 Action
store.dispatch(action)
2.然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State
let nextState = countReduce(previousState, action)
3.State 一旦有变化,Store 就会调用监听函数
// 设置监听函数
store.subscribe(listener)
4.listener可以通过store.getState()得到当前状态。如果使用的是 React,这时可以触发重新渲染 View
function listerner() {
let newState = store.getState();
component.setState(newState);
}
下面是是一个redux使用的案例
通过createStore
来创建一个store
,创建成功后会返回三个API(subscribe
、dispatch
、getState
)。我们通过subscribe
来订阅store中数据的变化,当有变化时会执行回调函数,通过getState
获取最新数据输出,最后我们通过dispatch
传入action
来触发数据改变。
import { createStore } from 'redux'
const reducer = (state = {count: 0}, action) => {
switch (action.type){
case 'INCREASE': return {count: state.count + 1};
case 'DECREASE': return {count: state.count - 1};
default: return state;
}
}
const actions = {
increase: () => ({type: 'INCREASE'}),
decrease: () => ({type: 'DECREASE'})
}
const store = createStore(reducer);
// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
);
// 打印初始状态
console.log(store.getState())
// 发起一系列 action
store.dispatch(actions.increase()) // {count: 1}
store.dispatch(actions.increase()) // {count: 2}
store.dispatch(actions.increase()) // {count: 3}
// 停止监听 state 更新
unsubscribe()
我们知道Action发出以后,Reducer中立即计算出新的State,这种叫做同步;Action发出以后,过一段时间再执行 Reducer,这就是异步。怎么才能让Reducer 在异步操作结束后自动执行呢?这就要用到新的工具:中间件(middleware)
Redux本身就提供了非常强大的数据流管理功能,但这并不是它唯一的强大之处,它还提供了利用中间件来扩展自身功能
为了理解中间件,让我们站在框架作者的角度思考问题:如果要添加功能,你会在哪个环节添加?
想来想去,只有在发送 Action 的这个步骤,即store.dispatch()方法,可以添加功能。举例来说,要添加日志功能,把 Action 和 State 打印出来,可以对store.dispatch进行如下改造。
简单来讲,Redux middleware 提供了一个分类处理 action 的机会。在 middleware 中,我们可以检阅每一个流过的 action,并挑选出特定类型的 action 进行相应操作,以此来改变 action。这样说起来可能会有点抽象,我们直接来看图,这是在没有中间件情况下的 redux 的数据流:
上面是很典型的一次 redux 的数据流的过程,但在增加了 middleware 后,我们就可以在这途中对 action 进行截获,并进行改变。且由于业务场景的多样性,单纯的修改 dispatch 和 reduce 人显然不能满足大家的需要,因此对 redux middleware 的设计是可以自由组合,自由插拔的插件机制。也正是由于这个机制,我们在使用 middleware 时,我们可以通过串联不同的 middleware 来满足日常的开发,每一个 middleware 都可以处理一个相对独立的业务需求且相互串联:
如上图所示,派发给 redux Store 的 action 对象,会被 Store 上的多个中间件依次处理,如果把 action 和当前的 state 交给 reducer 处理的过程看做默认存在的中间件,那么其实所有的对 action 的处理都可以有中间件组成的。值得注意的是这些中间件会按照指定的顺序一次处理传入的 action,只有排在前面的中间件完成任务之后,后面的中间件才有机会继续处理 action,同样的,每个中间件都有自己的“熔断”处理,当它认为这个 action 不需要后面的中间件进行处理时,后面的中间件也就不能再对这个 action 进行处理了。
而不同的中间件之所以可以组合使用,是因为 Redux 要求所有的中间件必须提供统一的接口,每个中间件的逻辑虽然不一样,但只要遵循统一的接口就能和redux以及其他的中间件对话了。
由于redux 提供了 applyMiddleware 方法来加载 middleware,因此我们首先可以看一下 redux 中关于 applyMiddleware 的源码:
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
// 利用传入的createStore和reducer和创建一个store
const store = createStore(...args)
let dispatch = () => {
throw new Error(
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 接着 compose 将 chain 中的所有匿名函数,组装成一个新的函数,即新的 dispatch
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
从上面的代码我们不难看出,applyMiddleware 这个函数的核心就在于在于组合 compose,通过将不同的 middlewares 一层一层包裹到原生的 dispatch 之上,然后对 middleware 的设计采用柯里化的方式,以便于compose ,从而可以动态产生 next 方法以及保持 store 的一致性。
说起来可能有点绕,直接来看一个啥都不干的中间件是如何实现的:
const doNothingMidddleware = (dispatch, getState) => next => action => next(action)
上面这个函数接受一个对象作为参数,对象的参数上有两个字段 dispatch 和 getState,分别代表着 Redux Store 上的两个同名函数,但需要注意的是并不是所有的中间件都会用到这两个函数。然后 doNothingMidddleware 返回的函数接受一个 next 类型的参数,这个 next 是一个函数,如果调用了它,就代表着这个中间件完成了自己的职能,并将对 action 控制权交予下一个中间件。但需要注意的是,这个函数还不是处理 action 对象的函数,它所返回的那个以 action 为参数的函数才是。最后以 action 为参数的函数对传入的action 对象进行处理,在这个地方可以进行操作,比如:
1)调动dispatch派发一个新 action 对象
2)调用 getState 获得当前 Redux Store 上的状态
3)调用 next 告诉 Redux 当前中间件工作完毕,让 Redux 调用下一个中间件
4)访问 action 对象 action 上的所有数据
在具有上面这些功能后,一个中间件就足够获取 Store 上的所有信息,也具有足够能力可用之数据的流转。看完上面这个最简单的中间件,下面我们来看一下 redux 中间件内,最出名的中间件 redux-thunk 的实现:
unction 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;
redux-thunk的代码很简单,它通过函数是变成的思想来设计的,它让每个函数的功能都尽可能的小,然后通过函数的嵌套组合来实现复杂的功能,我上面写的那个最简单的中间件也是如此(当然那是个瓜皮中间件)。redux-thunk 中间件的功能也很简单。首先检查参数 action 的类型,如果是函数的话,就执行这个 action,并把 dispatch, getState, extraArgument 作为参数传递进去,否则就调用 next 让下一个中间件继续处理 action 。
需要注意的是,每个中间件最里层处理 action 参数的函数返回值都会影响 Store 上的 dispatch 函数的返回值,但每个中间件中这个函数返回值可能都不一样。就比如上面这个 react-thunk 中间件,返回的可能是一个 action 函数,也有可能返回的是下一个中间件返回的结果。因此,dispatch 函数调用的返回结果通常是不可控的,我们最好不要依赖于 dispatch 函数的返回值。
前面我们已经对redux-thunk进行了讨论,它通过多参数的 currying(柯里化函数) 以实现对函数的惰性求值,从而将同步的 action 转为异步的 action。在理解了redux-thunk后,我们在实现数据请求时,action就可以这么写了:
function fetchData(url, params) {
return (dispatch, getState) => {
dispatch({
type: 'FETCHR_INIT', // 请求开始
});
fetch(url, params)
.then(result => {
dispatch({
type: 'FETCHR_SUCCESS', data: result, // 请求成功
});
})
.catch(err => {
dispatch({
type: 'FETCH_ERROR', error: err, //请求失败
});
});
};
}
以上就是redux-thunk的简单用法
其实还有很多常用的Redux异步请求的库如:redux-promise, redux-saga等。
总结:其实Redux中间件就是对dispatch方法的封装
每个中间件必须要定义成一个函数,返回一个接受 next参数的函数,而这个接受next参数的函数又返回一个接受 action参数的函数 。 next参数本身也是一个函数,中间 件调用这个 next 函数通知 Redux 自己的处理工作已经结束 。
react-redux插件是专门用于在React中使用redux的,具有高效且灵活的特性。 react-redux官网地址。
本质上 react-redux也是react高阶组件HOC的一种实现。其基于 容器组件和展示组件相分离 的开发思想来实现的。
其核心是通过两部分来实现:
1、Provider
2、container通过connect来解除手动调用store.subscrible。
Provider 是react-redux向我们提供的用来作为 顶层组件 的普通组件。它只需要一个属性——store,用来存放我们的顶层 state 然后将它分发给所有 connect 的组件,不论它在哪儿。
作用:所有的容器组件都需要能够访问Redux的store,如果不使用Provider组件,就只能在所有容器组件的属性中传入store,假如在组件树中深层嵌套了容器组件,可能有的展示组件也需要传入store属性。但是使用Provider组件包裹上应用根组件后,应用中的所有容器组件就都能访问到Redux的store了。
provider用法如下,绑定之后,再经过connect处理,就可以在组件中通过props访问对应信息了。
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp)
render(
// 绑定store
document.getElementById('root')
)
其实react-redux提供的Provide组件是基于react的 React.createContext实现的。
Provider将store传递给子组件,具体如何和组件绑定就是conect做的事情了。
import MyContext from "./MyContext";
// MyContext就是通过React.createContext()生成context const MyContext = React.createContext(null);
const Provider = ({ store, children }) => {
return (
{children}
);
};
export default Provider;
React-Redux 提供connect方法,用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来。是一个柯里化(Currying)的函数,它先要接受两个参数:(数据绑定mapStateToProps 和 事件绑定 mapDispatchToProps),再接收一个参数,就是要绑定的组件本身。
作用:connect连接组件和store,该操作并不修改原组件而是返回一个新的增强了关联store的组件。
connect的使用
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
connect
有两个参数,分别是 mapStateToProps
,mapDispatchToProps
。
(1) mapStateToProps负责输入逻辑,即将state
映射到 UI
组件的参数(props
)(外部的数据(即state对象)如何转换为UI 组件的参数)
(2) mapDispatchToProps负责输出逻辑,即将用户对UI
组件的操作映射成 Action
。(用户发出的动作如何变为 Action对象,从 UI 组件传出去)
返回值,是一个对象.
从字面上理解,就是把状态(store
中的state
)映射成属性(组件中的props
)
const mapStateToProps = (state) => ( // 正常我们在react-redux中会这样书写
{number: state.number}
)
这样我们在组件中通过this.props就可以获取store中的数据number
mapDispatchToProps
是connect
函数的第二个参数,用来建立 UI
组件的参数到store.dispatch
方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action
,传给 Store
。它可以是一个函数,也可以是一个对象。
如果mapDispatchToProps
是一个函数,会把dispatch
当做一个参数传进去。
const mapDispatchToProps = (dispatch) => {
return {
onClick: () => {
dispatch({
type: 'ADD'
});
}
};
}
// 返回了一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。
如果mapDispatchToProps
是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。
bindActionCreators(mapDispatchToProps,this.store.dispatch)
我们来看一个结合了React-Redux事例。
下面是一个计数器组件,它是一个纯的 UI 组件。所有的数据都是从store中取。
class Counter extends Component {
render() {
const { value, onIncrease } = this.props
return (
{value}
)
}
}
上面代码中,这个 UI 组件有两个参数:value和onIncrease。前者需要从state计算得到,后者需要向外发出 Action。
接着,定义value到state的映射,以及onIncrease到dispatch的映射。
function mapStateToProps(state) {
return {
value: state.count
}
}
function mapDispatchToProps(dispatch) {
return {
onIncrease: () => dispatch(increaseAction)
}
}
// Action Creator
const increaseAction = { type: 'INCREASE' }
然后,使用connect方法生成容器组件。
const App = connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
然后,定义这个组件的 Reducer。
// Reducer
function counter(state = { count: 0 }, action) {
const count = state.count
switch (action.type) {
case 'INCREASE':
return { count: count + 1 }
default:
return state
}
}
最后,生成store对象,并使用Provider在根组件外面包一层。
const persistedState = loadState();
const store = createStore(
counter,
);
ReactDOM.render(
,
document.getElementById('root')
);
虽然这个例子可能很简单,但基本包含了React-Redux的基本用法。
1)通过context可以让Context.Provider子组件都能够访问到同一个数据,通过修改Context.Provider的value可以去重新渲染子组件的数据
2)通过将store放到context让所有子组件去通过redux的模式去修改渲染state
3)直接在组件中混入redux相关内容导致组件复用性很差,所以将redux相关逻辑放入connect封装的高阶组件中,然后通过props的形式传递state给原组件
4)有组件都会接收全部state,需要通过mapStateToProps来描述接收哪些state;修改state也需要获取到dispatch,通过mapDispatchToProps来获取dispatch进行state修改。
5)修改任意state都会导致所有组件重新渲染,原因是mapStateToProps、mapDispatchToProps会返回一个新的对象导致props更新,需要通过在Connect中的shouldComponentUpdate来对两次使用到的state和外部直接传入的props进行对比再决定是否重新渲染
如果组件需要用到的数据用mapStateToProps映射到组件中,如果用不到就不需要映射。
最后说白了React-Redux做了两件事,1.作为最顶级的组件,向子组件们分发状态,来让 React 组件响应式地渲染。2.监听子组件的回调,事件有权利回到最顶层影响顶层状态。
参考《 深入浅出React和Redux 》以及https://redux.js.org/