使用redux+react已有一段时间,刚开始使用并未深入了解其源码,最近静下心细读源码,感触颇深~
本文主要包含Redux设计思想、源码解析、Redux应用实例应用三个方面。
背景:
React 组件 componentDidMount 的时候初始化 Model,并监听 Model 的 change 事件,当 Model 发生改变时调用 React 组件的 setState 方法重新 render 整个组件,最后在组件 componentWillUnmount 的时候取消监听并销毁 Model。
最开始实现一个简单实例:例如add加法操作,只需要通过React中 setState 去控制变量增加的状态,非常简单方便。
但是当我们需要在项目中增加乘法/除法/幂等等复杂操作时,就需要设计多个state来控制views的改变,当项目变大,里面包含状态过多时,代码就变得难以维护并且state的变化不可预测。可能需要增加一个小功能时,就会引起多处改变,导致开发效率降低,代码可读性不高。
例如以往使用较多backbone形式:
如上图所示,可以看到 Model 和 View 之间关系复杂,后期代码难以维护。
为了解决上述问题,在 React 中引入了 Redux。Redux 是 JavaScript 状态容器,提供可预测化的状态管理方案。下面详细介绍~~
目的:
1、深入理解Redux的设计思想
2、剖析Redux源码,并结合实际应用对源码有更深层次的理解
3、实际工程应用中所遇到的问题总结,避免再次踩坑
一、Redux设计思想
背景:
传统 View 和 Model :一个 view 可能和多个 model 相关,一个 model 也可能和多个 view 相关,项目复杂后代码耦合度太高,难以维护。
redux 应运而生,redux 中核心概念reducer,将所有复杂的 state 集中管理,view 层用户的操作不能直接改变 state从而将view 和 data 解耦。redux 把传统MVC中的 controller 拆分为action和reducer
设计思想:
(1)Web 应用是一个状态机,视图与状态是一一对应的。
(2)所有的状态,保存在一个对象里面。
Redux 让应用的状态变化变得可预测。如果想改变应用的状态,就必须 dispatch 对应的 action。而不能直接改变应用的状态,因为保存这些状态的地方(称为 store)只有 get方法(getState) 而没有 set方法。
只要Redux 订阅(subscribe)相应框架(例如React)内部方法,就可以使用该应用框架保证数据流动的一致性。
Action Creator:
只能通过dispatch action来改变state,这是唯一的方法
action通常的形式是: action = { type: ‘ … ‘, data: data } action一定是有一个type属性的对象
在dispatch任何一个 action 时将所有订阅的监听器都执行,通知它们有state的更新
var listeners = currentListeners = nextListeners //更新currentListeners
for (var i = 0; i < listeners.length; i++) { //触发所有已订阅的listener,通知其消息
listeners[i]()
}
Store:
Redux中只有一个store,store中保存应用的所有状态;判断需要更改的状态分配给reducer去处理。
可以有多个reducer,每个reducer去负责一小部分功能,最终将多个reducer合并为一个根reducer
作用:
- 维持state树;
- 提供 getState() 方法获取 state;
- 提供 dispatch(action) 方法更新 state;
- 通过 subscribe(listener) 注册监听器。
Reducer:
store想要知道一个action触发后如何改变状态,会执行reducer。reducer是纯函数,根reducer拆分为多个小reducer ,每个reducer去处理与自身相关的state更新
注:不直接修改整个应用的状态树,而是将状态树的每一部分进行拷贝并修改拷贝后的变量,然后将这些部分重新组合成一颗新的状态树。应用了数据不可变性(immutable),易于追踪数据改变。此外,还可以增加例如撤销操作等功能。
Views:
容器型组件 Container component 和展示型组件 Presentational component)
建议是只在最顶层组件(如路由操作)里使用 Redux。其余内部组件仅仅是展示性的,所有数据都通过 props 传入。
容器组件 | 展示组件 | |
---|---|---|
Location | 最顶层,路由处理 | 中间和子组件 |
Aware of Redux | 是 | 否 |
读取数据 | 从 Redux 获取 state | 从 props 获取数据 |
修改数据 | 向 Redux 派发 actions | 从 props 调用回调函数 |
Middleware:
中间件是在action被发起之后,到达reducer之前对store.dispatch方法进行扩展,增强其功能。
例如常用的异步action => redux-thunk、redux-promise、redux-logger等
Redux中store、action、views、reducers、middleware等数据流程图如下:
简化数据流程图:
Redux核心:
- 单一数据源,即:整个Web应用,只有一个Store,存储着所有的数据【数据结构嵌套太深,数据访问变得繁琐】,保证整个数据流程是Predictable。
- 将一个个reducer自上而下一级一级地合并起,最终得到一个rootReducer。 => Redux通过一个个reducer完成了对整个数据源(object tree)的拆解访问和修改。 => Redux通过一个个reducer实现了不可变数据(immutability)。
- 所有数据都是只读的,不能修改。想要修改只能通过dispatch(action)来改变state。
二、Redux源码解析
前记— redux的源码比较直观简洁~
Redux概念和API,请直接查看官方英文API和官方中文API
Redux目录结构:
|---src
|---applyMiddleware.js
|---bindActionCreators.js
|---combineReducers.js
|---compose.js
|---createStore.js 定义createStore
|---index.js redux主文件,对外暴露了几个核心API
以下分别是各个文件源码解析(带中文批注):
1) combineReducers.js
- 实质:组合多个分支reducer并返回一个新的reducer,参数也是state和action,进行state的更新处理
- 初始化:store.getState()的初始值为reducer(initialState, { type: ActionTypes.INIT })
- Reference:http://cn.redux.js.org//docs/api/combineReducers.html
/**
* 把多个reducer合并为一个reducer
* @param reducers {array} 多个reducer组成的数组
* @return {object} 全局state
*/
export default function combineReducers(reducers) {
var reducerKeys = Object.keys(reducers)
var finalReducers = {}
.....// 数据处理
return function combination(state = {}, action) {// 返回一个需要传入state和action参数的方法,该方法的返回值是最新的state
.....// 检查是否包含多个reducer, state的字段是否和reducer的key对应
var hasChanged = false
var nextState = {}
/**
* 对传入combineReducers的 state 每个字段(var previousStateForKey = state[key])都和
* 处理后的新state(var nextStateForKey = reducer(previousStateForKey, action))对比
* 如果state里对应的字段内容相同,hasChanged 为false
* 目的:逐个更新每个reducer中对应的state,最终一个循环更新完所有的state
*/
for (var i = 0; i < finalReducerKeys.length; i++) {
var key = finalReducerKeys[i]
var reducer = finalReducers[key]
var previousStateForKey = state[key]
var nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
// 1. action type 存在,返回新的state, hasChanged 为 true
// 2. action type 不存在,返回原来的state, hasChanged 为 false
// 3. 不管action type 是否存在, 在原来的state上修改,但是返回的是修改后的state(没有返回拷贝), hasChanged还是为false
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 根据更改状态来决定返回的state值
// state 的更新应该在"不可变(immutable)"的理念下完成 => 总是去返回一个新的更新后的对象,而不是直接去修改原始的 state tree。
return hasChanged ? nextState : state
}
}
/**
* combineReducer 接收一个拆分后 reducer 函数组成的对象,返回一个新的 Reducer 函数。
*/
combineReducers() 所做的只是生成一个函数,这个函数来调用一系列reducer,每个reducer根据它们的key来筛选出state中的一部分数据并处理,然后这个生成的函数再将所有reducer的结果合并成一个最终的state对象。
在实际应用中,reducer中对于state的处理是新生成一个state对象(深拷贝):
export default function(state = { }, action) {
if(action.data) {
let newState = {};
$.extend(true, newState, state);
switch (action.type) {
case 'DELETE_URL_FAIL':
$.extend(true, newState, action.data);
return newState;
case 'DELETE_URL_SUCCESS':
....
default:
return state;
}
}else {
return state;
}
}
因此在combineReducers中每个小reducers的 nextStateForKey !== previousStateForKey 一定为 true => hasChange也一定为true
那么问题来了,为什么要每次都拷贝一个新的state,返回一个新的state呢?
解释:
- Reducer 只是一些纯函数,它接收之前的 state 和 action,并返回新的 state。刚开始可能只有一个 reducer,随着应用变大,把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分,因为 reducer 只是函数,可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来处理一些通用任务,如分页器等。因为Reducer是纯函数,因此在reducer内部直接修改state是副作用,而返回新值是纯函数,可靠性增强,便于追踪bug。
- 此外由于不可变数据结构总是修改引用,指向同一个数据结构树,而不是直接修改数据,可以保留任意一个历史状态,这样就可以做到react diff从而局部刷新dom,也就是react非常快速的原因。
- 因为严格限定函数纯度,所以每个action做了什么和会做什么总是固定的,甚至可以把action存到一个栈里,然后逆推出以前的所有state,即react dev tools的工作原理。再提及react,一般来说操作dom只能通过副作用,然而react的组件都是纯函数,它们总是被动地直接展现store中得内容,也就是说,好的组件,不受外部环境干扰,永远是可靠的,出了bug只能在外面的逻辑层。这样写好纯的virtual dom组件,交给react处理副作用,很好地分离了关注点。
2) applyMiddleware.js
实质:利用中间件来包装store的dispatch方法,如果有多个middleware则需要使用compose函数来组合,从右到左依次执行middleware
Reference:applymiddleware方法、middleware介绍
import compose from './compose'
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
//获取最基本的store对象,其中包含dispatch、subscribe、getState和replaceReducer四种方法的对象
//相当于调用 Redux.createStore(reducer, preloadedState)
var store = createStore(reducer, preloadedState, enhancer)
var dispatch = store.dispatch //最原始的dispatch方法
var chain = []
// 中间件API,包含getState和dispatch函数
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action) //原始dispatch被覆盖,变成包装后的dispatch函数
}
chain = middlewares.map(middleware => middleware(middlewareAPI)) //给每个middleware传入基本的getState和dispatch的API方法
dispatch = compose(...chain)(store.dispatch) //用所有传入的middlewares来包装store.dispatch,但其实最终middleware中使用的dispatch方法都是原生store.dispatch
return { //更新store的dispatch方法,利用中间件来增强store.dispatch函数的功能
...store,
dispatch
}
}
}
Reducer有很多很有意思的中间件,可以参考中间件
3) createStore.js
实质:
- 若不需要使用中间件,则创建一个包含dispatch、getState、replaceReducer、subscribe四种方法的对象
- 若使用中间件,则利用中间件来包装store对象中的dispatch函数来实现更多的功能
createStore.js中代码简单易读,很容易理解~
(警告)注:
redux.createStore(reducer, preloadedState, enhancer)
如果传入了enhancer函数,则返回 enhancer(createStore)(reducer, preloadedState)
如果未传入enhancer函数,则返回一个store对象,如下:
store = {
dispatch,
subscribe,
getState,
replaceReducer,
}
- store对象对外暴露了dispatch、getState、subscribe、replaceReducer方法
- store对象通过getState() 获取内部最新state
- preloadedState为 store 的初始状态,如果不传则为undefined
- store对象通过reducer来修改内部state值
- store对象创建的时候,内部会主动调用dispatch({ type: ActionTypes.INIT })来对内部状态进行初始化。通过断点或者日志打印就可以看到,store对象创建的同时,reducer就会被调用进行初始化。
Reference:http://cn.redux.js.org/docs/api/Store.html
/**
* INIT是内部私有的type
* 作用:传这个type到reducer时,reducer中没有对应的type,所以会命中default返回默认state
* 注:自己命名的type不要和内部私有type名重复
* 初始化时,redux.createStore(reducer, initialState)时,传入INIT作为action.type
*/
export var ActionTypes = { INIT: '@@redux/INIT' }
/**
* 创建store
* 参数reducer,类型是function
* 参数preloadedState,没有类型限制,是初始的state结构
* 参数enhancer,类型是function,是redux的中间件
* 返回一个redux store,在store中可以读取state/分发actions/订阅任何改变
*/
export default function createStore(reducer, preloadedState, enhancer) {
......
if (typeof enhancer !== 'undefined') { //函数传入了增强器参数
.....//增强器不是函数时报错
return enhancer(createStore)(reducer, preloadedState)//给增强器多级传入参数,enhancer函数实际理解请看applymiddleware及其传入的中间件
}
......//赋值操作
/**
* 确保nextListeners可以改变[//断开nextListeners和currentListeners的引用,两个数组可以分别操作,而不会影响另一个]
*/
function ensureCanMutateNextListeners() { ..... }
/**
* 返回当前最新的state
*/
function getState() { return currentState }
/**
* 订阅监听器
* 参数listener,类型是函数,在每个分发action后执行的回调函数
* 返回一个函数,移除该监听器的订阅
* 注:当数据发生改变时想要得到通知,都需要依靠redux中维护listener数组,订阅之后都可以收到通知
*/
function subscribe(listener) {
......//基本类型检查
var isSubscribed = true
ensureCanMutateNextListeners() //将nextListeners和currentListener断开引用
nextListeners.push(listener) //把想要订阅的监听器增加到nextListeners中
return function unsubscribe() { //返回一个取消该监听器订阅的函数
if (!isSubscribed) {
return
}
isSubscribed = false
ensureCanMutateNextListeners() //将nextListeners和currentListener断开引用
var index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1) //把想要订阅的监听器从nextListeners中移除
}
}
/**
* 分发action => 触发state变化的唯一途径
* 参数action,类型是对象,有一个type非空的属性
* 返回值:要分发的action
*/
function dispatch(action) {
.....//action类型检测&&type属性检测
try {
isDispatching = true //更新分发状态
//将action中携带的数据和当前的currentState传入reducer函数中,reducer函数中进行处理后得到新的state即currentState。最终达到更新state的目的
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false //更新分发状态
}
var listeners = currentListeners = nextListeners //更新currentListeners
for (var i = 0; i < listeners.length; i++) { //触发所有已订阅的listener,通知其state更新消息
listeners[i]()
}
return action
}
/**
* 动态替换原来createStore中传入的reducer为nextReducer
* 参数nextReducer,类型是函数,新的reducer
*/
function replaceReducer(nextReducer) { ..... }
dispatch({ type: ActionTypes.INIT }) //当最开始store创建时,“INIT”的action.type被分发给reducer,返回initialState
return {//若createStore未传入增强器参数,则返回一个有dispatch、subscribe、getState和replaceReducer四种方法的对象
dispatch,
subscribe,
getState,
replaceReducer
}
}
考虑实际应用中通常使用的中间件thunk和logger:
- thunk源码:
function 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;
- logger源码:
/**
* 记录所有被发起的 action 以及产生的新的 state。
*/
const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd(action.type)
return result
}
整个store包装流程:
4) bindActionCreators.js
- 实质:将所有的action都用dispatch包装,方便调用
- Reference:http://cn.redux.js.org//docs/api/bindActionCreators.html
/**例如 actionCreator = {add: add, reduce: reduce}
* => bindActionCreator(actionCreator, dispatch)
* => {
* add: function(args) { dispatch( actionCreator.add(args) ) },
* reduce: function(args) { dispatch( actionCreator.reduce(args) ) }
* }
*/
function bindActionCreator(actionCreator, dispatch) { //分发action
.....
}
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') { // 基本类型检查
return bindActionCreator(actionCreators, dispatch)
}
......// 初始值检测报错
var keys = Object.keys(actionCreators)
var boundActionCreators = {}
for (var i = 0; i < keys.length; i++) { //分发所有的action,将所有的action都dispatch,这样可以在container中直接调用action函数
var key = keys[i]
var actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators //返回所有dispatch后的action,方便直接调用
}
5) compose.js
- 实质:组合多个Redux的中间件
- Reference:http://cn.redux.js.org//docs/api/compose.html
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
const last = funcs[funcs.length - 1]
const rest = funcs.slice(0, -1)
// last(...args)是作为reduceRight方法初始化值
// compose函数作为管道,从右到左来组合多个middleware
// compose函数只是不使用深度右括号来写深度嵌套的函数,利用管道机制即前一个函数的输出是后一个函数的输入
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}
6) index.js
- 实质:抛出Redux中几个重要的API函数
三、实例应用Redux
Redux的核心思想:Action、Store、Reducer、UI View配合来实现JS中复杂的状态管理,详细讲解请查看:Redux基础
React+Redux结合应用的工程目录结构如下:
|—actions
addAction.js
reduceAction.js
|—components
|—dialog
|—pagination
|—constant
|—containers
|---add
addContainer.js
add.less
|—reduce
reduceContainer.js
reduce.less
|—reducers
addReducer.js
reduceReducer.js
|—setting
setting.js
|—store
configureStore.js
|—entry
index.js
|—template
index.html
优势:明确代码依赖,减少耦合,降低复杂度~~
下面是实际工程应用中使用react+redux框架进行重构时,总结使用redux时所涉及部分问题&&需要注意的点:
1. Store
在创建新的store即createStore时,需要传入由根Reducer、初始化的state树及应用中间件。
1)根Reducer
重构的工程应用代码很多,不可能让全部state的变更都通过一个reducer来处理。需要拆分为多个小reducer,最后通过combineReducers来将多个小reducer合并为一个根reducer。拆分reducer时,每个reducer负责state中一部分数据,最终将处理后的数据合并成为整个state。注意每个reducer只负责管理全局state中它负责的一部分。每个 reducer的state参数都不同,分别对应它管理的那部分state数据。
实际工程代码重构中以功能来拆分reducer:
let web = combineReducers({
index,
header,
menu,
footer,
......
routing: routerReducer
});
是es6中对象的写法,每个reducer所负责的state可以更改属性名。
2)initialState => State树
设计state结构:在Redux应用中,所有state都被保存在一个单一对象中,其中包括工程全局state,因此对于整个重构工程而言,提前设计state结构显得十分重要。
尽可能把state范式化:大部分程序处理的数据都是嵌套或互相关联的,开发复杂应用时,尽可能将state范式化,不存在嵌套。可参考State范式化
2、Action
唯一触发更改state的入口,通常是dispatch不同的action。
API请求尽量都放在Action中,但发送请求成功中返回数据不同情况尽量在Reducer中进行处理。
- action.js:
fetchRssList: () => (dispatch, getState) => {
$.ajax({
type: 'GET',
url: CONSTANT.GET_LIST,
dataType: 'json',
})
.done((res) => {
dispatch({
type: 'GET_DATA_LIST',
data: res
})
})
.fail(() => {
dispatch({
type: 'GET_DATA_FAIL',
data: '请求失败,请刷新重试'
})
})
}
- reducer.js
export default function(state = { }, action) {
if(action.data) {
let newState = {};
$.extend(true, newState, state);
switch (action.type) {
case 'GET_DATA_LIST':
if(action.data.message == 'success') {
newState.list = action.data
}else {
newState.errorReason = '请求错误,请重试';
}
return newState;
case 'GET_DATA_FAIL':
newState.errorReason = '网络错误,请重试';
return newState;
default:
return state;
}
}else {
return state;
}
}
注:
1、如若在请求发送后,需要根据返回数据来判断是否需要发送其他请求或者执行一些非纯函数,那么可以将返回数据不同情况的处理在Action中进行。
2、假设遇到请求错误,需要给用户展示错误原因,如上述reducer代码中errorReason。 需要考虑到是否可能会在提示中增加DOM元素或者一些交互操作,因此最好是将errorReason在action中赋值,最后在reducer中进行数据处理【reducer是纯函数】。
- action.js
fetchList: () => (dispatch, getState) => {
$.ajax({
type: 'GET',
url: CONSTANT.GET_LIST,
dataType: 'json',
})
.done((res) => {
if(res.message !== 'success') {
fetchList();
}else {
dispatch({
type: 'GET_DATA_LIST',
data: res
})
}
})
.fail(() => {
dispatch({
type: 'GET_DATA_FAIL',
data: '请求失败,请刷新重试'
})
})
}
3、Reducer
reducer是一个接收旧state和action,返回新state的函数。 (prevState, action) => newState
切记要保持reducer纯净,只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。永远不要在reducer中做这些操作:
a、修改传入参数
b、执行有副作用的操作,如API请求和路由跳转等
c、调用非纯函数,例如Date.now() 或 Math.random()
永远不要修改旧state!比如,reducer 里不要使用 Object.assign(state, newData),应该使用Object.assign({}, state, newData)。这样才不会覆盖旧的 state。
- reducer.js:
export default function(state = { }, action) {
if(action.data) {
let newState = {};
$.extend(true, newState, state);
switch (action.type) {
case 'GET_DATA_LIST':
if(action.data.message == 'success') {
newState.list = action.data
}else {
newState.errorReason = '请求错误,请重试';
}
return newState;
case 'GET_DATA_FAIL':
newState.errorReason = '网络错误,请重试';
return newState;
default:
return state;
}
}else {
return state;
}
}
4、View(Container)
渲染界面
a、mapStateToProps
利用mapStateToProps可以拿到全局state,但是当前页面只需要该页面的所负责部分state数据,因此在给mapStateToProps传参数时,只需要传当前页面所涉及的state。因此在对应的reducer中,接收的旧state也是当前页面所涉及的state值。
b、mapDispatchToProps
在mapDispatchToProps中利用bindActionCreators让store中dispatch页面所有的Action,以props的形式调用对应的action函数。
所有的 dispatch action 均由 container 注入 props 方式实现。
c、connect ( react-redux )
react-redux 提供的 connect() 方法将组件连接到 Redux,将应用中的任何一个组件connect()到Redux Store中。被connect()包装好的组件都可以得到一些方法作为组件的props,并且可以得到全局state中的任何内容。
connect中封装了shouldComponentUpdate方法
shouldComponentUpdate() {
return !pure || this.haveOwnPropsChanged || this.hasStoreStateChanged
}
如果state保持不变那么并不会造成重复渲染的问题,内部组件还是使用mapStateToProps方法选择该组件所需要的state。需要注意的是:单独的功能模块不能使用其他模块的state.
d、bind
在constructor中bind所有event handlers => bind方法会在每次render时都重新返回一个指向指定作用域的新函数
- container.js
......
class Abc extends Component {
constructor(props) {
super(props);
this.handleClick = this.onClickDiv.bind(this);
}
renderTable(item, index) {
var that = this;
return (<div className='num' onClick={that.handleClick}>{index + 1}</div>)
}
onClickDiv() {
....
}
render() {
const { list, .... } = this.props;
const {
....
} = this.props.actions;
return (
<div className='pgc-abc'>
<div className='abc'>
{ list.map( (item, index) => this.renderTable(item, index))}
</div>
</div>
)
}
}
.....
export default connect(
mapStateToProps,
mapDispatchToProps
)(Abc)
四、总结
整篇文章主要是源码理解和具体项目应用中整个Redux处理state的流程,我对Redux有了更深层次的理解。
Redux+React已广泛应用,期待在未来的使用过程中,有更多更深刻的理解~
如有错误,欢迎指正 (~ ̄▽ ̄)~
参考链接:
- redux系列源码解析:http://div.io/topic/1530
- redux github:https://github.com/reactjs/redux
- redux剖析:https://egghead.io/lessons/javascript-redux-normalizing-the-state-shape