一.作用
与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:非常棒的文档,读起来根本停不下来