使用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的更新
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
combineReducers() 所做的只是生成一个函数,这个函数来调用一系列reducer,每个reducer根据它们的key来筛选出state中的一部分数据并处理,然后这个生成的函数再将所有reducer的结果合并成一个最终的state对象。
在实际应用中,reducer中对于state的处理是新生成一个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介绍
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、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
考虑实际应用中通常使用的中间件thunk和logger:
- thunk源码:
- logger源码:
整个store包装流程:
4) bindActionCreators.js
实质:将所有的action都用dispatch包装,方便调用
Reference:http://cn.redux.js.org//docs/api/bindActionCreators.html
5) compose.js
实质:组合多个Redux的中间件
[图片上传中...(image-ab545c-1525849044819-7)]
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:
[图片上传中...(image-ca8b5e-1525849044819-6)]
是es6中对象的写法,每个reducer所负责的state可以更改属性名。
2)initialState => State树
设计state结构:在Redux应用中,所有state都被保存在一个单一对象中,其中包括工程全局state,因此对于整个重构工程而言,提前设计state结构显得十分重要。
尽可能把state范式化:大部分程序处理的数据都是嵌套或互相关联的,开发复杂应用时,尽可能将state范式化,不存在嵌套。可参考State范式化
2、Action
唯一触发更改state的入口,通常是dispatch不同的action。
API请求尽量都放在Action中,但发送请求成功中返回数据不同情况尽量在Reducer中进行处理。
- action.js:
- reducer.js
注:
1、如若在请求发送后,需要根据返回数据来判断是否需要发送其他请求或者执行一些非纯函数,那么可以将返回数据不同情况的处理在Action中进行。
2、假设遇到请求错误,需要给用户展示错误原因,如上述reducer代码中errorReason。 需要考虑到是否可能会在提示中增加DOM元素或者一些交互操作,因此最好是将errorReason在action中赋值,最后在reducer中进行数据处理【reducer是纯函数】。
- action.js
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:
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方法
如果state保持不变那么并不会造成重复渲染的问题,内部组件还是使用mapStateToProps方法选择该组件所需要的state。需要注意的是:单独的功能模块不能使用其他模块的state.
d、bind
在constructor中bind所有event handlers => bind方法会在每次render时都重新返回一个指向指定作用域的新函数
- container.js
四、总结
整篇文章主要是源码理解和具体项目应用中整个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