一.作用
与Flux一样,作为状态管理层,对单向数据流做强约束
二.出发点
MVC中,数据(Model)、表现层(View)、逻辑(Controller)之间有明确的界限,但数据流是双向的,在大型应用中尤其明显。一个变化(用户输入或者内部接口调用)可能会影响应用的多处状态,例如双向数据绑定,很难维护调试
一个model可以更新另一个model的话,一个view更新一个model,这个model更新了另一个model,可能会引发另一个view的更新。不知道某一时刻应用到底发生了什么,因为不知道何时、为何、怎样发生的状态变化。系统不透明,很难复现bug和添加新特性
希望通过强制单向数据流来降低复杂度,提升可维护性和代码可预测性
三.核心理念
Redux用一棵不可变状态树维护整个应用的状态,无法直接改变,发生变化时,通过action
和reducer
创建新的对象,具体如下:
- 应用的状态对象没有
setter
,不允许直接修改 - 通过
dispatch action
来修改状态 - 通过
reducer
把action
和state
联系起来 - 由上层
reducer
把下层的组织起来,形成reducer
树,逐层计算得到state
函数式的reducer
是关键:
- 小(职责单一)
- 纯(没有副作用,不影响环境)
- 独立(不依赖环境,固定输入对应固定输出。容易测试,只用关注给定输入对应的返回值是否正确)
纯函数约束让一些强大的调试特性得以实现(否则状态回滚几乎是不可能的),通过DevTools精确追踪变化:
- 显示当前
state
、历史action
及对应的state
- 跳过某些
action
,快速组合出bug场景,不需要手动准备 - 状态重置(Reset),提交(Commit),回滚(Revert)
- 热加载,定位
reducer
问题,立即修改生效
四.结构
代码语言:javascript复制action 与Flux一样,就是事件,带有type和data(payload)
同样手动dispatch action
---
store 与Flux功能一样,但全局只有1个,实现上是一颗不可变的状态树
分发action,注册listener。每个action经过层层reducer得到新state
---
reducer 与arr.reduce(callback, [initialValue])作用类似
reducer相当于callback,输入当前state和action,输出新state
reducer
的概念相当于node中间件,或者gulp插件,每个reducer
负责状态树的一小部分,把一系列reducer
串联起来(把上一个reducer
的输出作为当前reducer
的输入),得到最终输出state
reducer
每次对state
的修改,都会创建一个新的state
对象,旧值指向原引用,新值被创建出来
严格的单向数据流:
代码语言:javascript复制 call new state
action --> store ------> reducers -----------> view
action
也是交给顶层的所有reducer
(与Flux类似),流向相应子树
store
负责协调,先把action
和当前state
传递给reducer
树,得到新state
,更新当前state
,再通知视图更新(React的话就是setState()
)
action
action
负责描述发生了什么(就像新闻标题)
action
与action creator
分别对应传统的event
和createEvent()
。需要action creator
是为了可移植和可测试
设计上把action creator
和store
分离是考虑服务端渲染,这样每个请求对应独立store
,由外部做action creator
和store
的绑定
注意:实践中应该把创建action
和dispatch action
解开,在需要的场景(比如传递给子组件,希望屏蔽dispatch
),Redux提供了bindActionCreators
再把它们两个绑起来
另外,考虑异步场景:
action
数量 一个异步操作可能需要3个action
(或者1个带有3种状态的action
),开始/成功/失败,对应的UI状态为显示loading/隐藏loading并显示新数据/隐藏loading并显示错误信息- 更新
view
的时机 异步操作结束后,dispatch action
修改state
,更新view
不用考虑多个异步操作的时序问题,因为从action
历史记录来看,顺序是固定不变的,同步还是异步过程中dispatch
的不重要
与同步场景没太大区别,只是action
多一些,一些中间件(redux-thunk、redux-promise等等)只是让异步控制形式上更优雅,从dispatch action
角度看没有区别
reducer
负责具体的状态更新(根据action
更新state
,让action
的描述成为事实)
相比Flux,Redux用纯函数reducer
来代替event emitter
:
- 分解与组合
通过拆分
reducer
来分解状态,再把reducer
组合起来(combineReducers()
工具函数)形成状态树,reducer组合在Redux应用里很常见(基本套路) 通常把1个reducer
拆成一组相似的reducer
(或者抽象出reducer factory
) - 单一职责
每一个
reducer
只负责全局状态的一部分
纯函数reducer
的具体约束(与FP中的纯函数概念一致)如下:
- 不修改参数
- 只是单纯的计算,不要掺杂副作用,比如路由切换之类的其它API调用
- 不要调用不纯(输出不单取决于输入,还与环境有关)的方法 比如
Math.random()
、new Date()
另外,reducer
与state
密切相关,state
是reducer
树的计算结果,所以需要先规划整个应用的state
结构,有一些非常好用的技巧:
- 把
state
分为数据状态和UI状态 UI状态可以维护在组件内部,也可以挂到状态树上,但都应该考虑区分数据状态和UI状态 (简单场景及UI状态变化可能不需要作为store
的一部分,而应该在组件级来维护) - 把
state
看做数据库 对于复杂的应用,应该把state
当做数据库,存放数据时建立索引,关联数据之间通过id来引用。这样相对独立,可以减少嵌套状态(嵌套状态会让state
子树越来越大,而数据表 关系表
就不会)
Store
胶水,用来组织action
和reducer
,并支持listener
负责3件事:
- 持有
state
,支持读写(getState()
读,dispatch(action)
写) - 接到
action
时,调度reducer
- 注册/解绑
listener
(每次状态变化时触发)
五.3个基本原则
整个应用对应一棵state树
这样很容易生成另外一份state
(保留历史版本),也很容易实现redo/undo
state只读
- 只能通过触发
action
来更新state
- 集中变更,且以严格顺序发生(没有需要特别小心的竞争条件)
- 而
action
都是纯对象,可以记录日志、序列化,存起来以后还能回放(调试/测试)
reducer都是纯函数
输入state
和action
,输出新state
。每次都返回新的,不维护(修改)输入的state
所以能随便调整reducer
执行顺序,放电影一样的调试控制得以实现
六.react-redux
Redux与React没有任何关系,Redux作为状态管理层可以配合任何UI方案使用,例如backbone、angular、React等等
react-redux用来处理new state -> view
的部分,也就是说,新state
有了,怎样同步视图?
container
也有container
和view
的概念(与Flux相同)
container
是一种特殊的组件,不含视图逻辑,与store
关系紧密。从逻辑功能上看就是通过store.subscribe()
读取状态树的一部分,作为props
传递给下方的普通组件(view
)
connect()
一个看起来很神奇的API,主要做3件事:
- 负责把
dispatch
和state
数据作为props
注入下方普通组件 - 往虚拟DOM树自动插入一些
container
- 内置性能优化,避免不必要的更新(内置
shouldComponentUpdate
)
七.Redux与Flux
相同点
- 把Model更新逻辑单独提出来作为一层(Redux的
reducer
,Flux的store
) - 都不允许直接更新
model
,而要求用action
描述每一个变化 (state, action) => state
的基本思路是一致的
不同点
- Redux是一种具体实现,而Flex是一种模式 Redux只有一个,而Flux有十好几种实现
- Redux的
state
是1棵树 Redux把应用状态挂在1棵树上,全局只有一个store
而Flux有多个store
,并把状态变更作为事件广播出去,组件通过订阅这些事件来同步当前状态 - Redux没有
dispatcher
的概念 因为依赖纯函数,而不是事件触发器。纯函数可以随便组合,不需要额外管理顺序 在Flux里dispatcher
负责把action
传递给所有store
- Redux假设不会手动修改
state
道德约束,不允许在reducer
里修改state
(可以添新属性,但不允许修改现有的) 不作为强约束是考虑某些性能场景,技术上可以通过写不纯的reducer
来解决 如果reducer
不纯的话,依赖纯函数组合特性的强大调试功能会被破坏,所以强烈不建议这么做 不强制state
用不可变的数据结构,是出于性能(不可变相关的额外处理)和灵活性(可以配合const
、immutablejs
等使用)考虑
八.问题与思考
1.state变化订阅机制的粒度控制是怎样的?
subscribe(listener)
只能得到全局完整state
,那么React setState()
粒度是怎样的,怎么分子树?
手动处理。state
树有任何变化都通知所有listener
,listener
里手动判断自己关注的那一小部分state
变了没。也就是订阅机制不管分发,需要手动分发
2.react-reduct的是怎么回事?
猜一下。应该是把store
挂在hostContainerInfo
上了,所以要求在render root
时把Provider
作为顶层容器:
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
hostContainerInfo
长这样子:
function ReactDOMContainerInfo(topLevelWrapper, node) {
var info = {
_topLevelWrapper: topLevelWrapper,
_idCounter: 1,
_ownerDocument: node ? node.nodeType === DOC_NODE_TYPE ? node : node.ownerDocument : null,
_node: node,
_tag: node ? node.nodeName.toLowerCase() : null,
_namespaceURI: node ? node.namespaceURI : null
};
if ("development" !== 'production') {
info._ancestorInfo = node ? validateDOMNesting.updatedAncestorInfo(null, info._tag, null) : null;
}
return info;
}
(摘自ReactDOM v15.5.4源码)
虚拟DOM树上所有组件共享hostContainerInfo
,所以store
在所有container
里都能访问,示例代码见Usage with React
3.树的场景(无限级展开)怎么处理?
一个典型的业务场景,无限级树结构,处理技巧在于把state看做数据库(前面提到过这个技巧)
按照Redux的理念,应该把tree
打平成nodes
,粗粒度可以是nodeId - children
,细粒度就是nodeId - node
(children
变成了childrenIdList
,再查总id表得到children
)
打平能够解决问题,比嵌套状态好维护得多,如果树组件对应一个tree
对象的话(node
都在tree
上),对一棵大树做局部更新会很难受
P.S.3NF竟然能应用在前端,简直难以置信!
参考资料
- Redux doc:非常棒的文档,读起来根本停不下来
- Redux · An Introduction