一.作用
与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
问题,立即修改生效
四.结构
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
对象,旧值指向原引用,新值被创建出来
严格的单向数据流:
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-redux的<Provider>是怎么回事?
猜一下,应该是通过(猜错了)所以要求在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
react-redux真实实现
猜错了,直接看吧
内部实例是私有属性(一个随机的key
,__reactInternalInstance&<random>
),所以组件无法访问hostContainerInfo
,但是React提供了一个增强版hostContainerInfo
,叫context
,专门应对需要深层手动传递props
的场景,大致是这样:
// Provider
class Provider extends React.Component {
constructor(props) {
super(props);
}
// 把顶层手动传入的store prop作为context属性
getChildContext() {
return {store: this.props.store};
}
render() {
return this.props.children;
}
}
// container
class Container extends React.Component {
// 把context里的store取出来,作为container的prop
// container里就可以通过this.props.store访问store了
getDefaultProps() {
return {
store: this.context.store;
}
}
}
用起来就像store
从顶层穿透到了所有组件,那么,技术上在普通组件(view
,非container
)里也可以通过this.context.store
直接访问store
(因为context
会向下无脑自动传递,无法控制),但这样做不太道德
P.S.一直不知道context
有什么用,终于明白了
3.树的场景(无限级展开)怎么处理?
一个典型的业务场景,无限级树结构,处理技巧在于把state看做数据库(前面提到过这个技巧)
按照Redux的理念,应该把tree
打平成nodes
,粗粒度可以是nodeId - children
,细粒度就是nodeId - node
(children
变成了childrenIdList
,再查总id表得到children
)
打平能够解决问题,比嵌套状态好维护得多,如果树组件对应一个tree
对象的话(node
都在tree
上),对一棵大树做局部更新会很难受
P.S.3NF竟然能应用在前端,简直难以置信!
参考资料
Redux doc:非常棒的文档,读起来根本停不下来