Redux

一.作用

与Flux一样,作为状态管理层,对单向数据流做强约束

二.出发点

MVC中,数据(Model)、表现层(View)、逻辑(Controller)之间有明确的界限,但数据流是双向的,在大型应用中尤其明显。一个变化(用户输入或者内部接口调用)可能会影响应用的多处状态,例如双向数据绑定,很难维护调试

一个model可以更新另一个model的话,一个view更新一个model,这个model更新了另一个model,可能会引发另一个view的更新。不知道某一时刻应用到底发生了什么,因为不知道何时、为何、怎样发生的状态变化。系统不透明,很难复现bug和添加新特性

希望通过强制单向数据流来降低复杂度,提升可维护性和代码可预测性

三.核心理念

Redux用一棵不可变状态树维护整个应用的状态,无法直接改变,发生变化时,通过actionreducer创建新的对象,具体如下:

  • 应用的状态对象没有setter,不允许直接修改

  • 通过dispatch action来修改状态

  • 通过reduceractionstate联系起来

  • 由上层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负责描述发生了什么(就像新闻标题)

actionaction creator分别对应传统的eventcreateEvent()。需要action creator是为了可移植和可测试

设计上把action creatorstore分离是考虑服务端渲染,这样每个请求对应独立store,由外部做action creatorstore的绑定

注意:实践中应该把创建actiondispatch 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()

另外,reducerstate密切相关,statereducer树的计算结果,所以需要先规划整个应用的state结构,有一些非常好用的技巧

  • state分为数据状态和UI状态

    UI状态可以维护在组件内部,也可以挂到状态树上,但都应该考虑区分数据状态和UI状态

    (简单场景及UI状态变化可能不需要作为store的一部分,而应该在组件级来维护)

  • state看做数据库

    对于复杂的应用,应该state当做数据库,存放数据时建立索引,关联数据之间通过id来引用。这样相对独立,可以减少嵌套状态(嵌套状态会让state子树越来越大,而数据表 + 关系表就不会)

Store

胶水,用来组织actionreducer,并支持listener

负责3件事:

  • 持有state,支持读写(getState()读,dispatch(action)写)

  • 接到action时,调度reducer

  • 注册/解绑listener(每次状态变化时触发)

五.3个基本原则

整个应用对应一棵state树

这样很容易生成另外一份state(保留历史版本),也很容易实现redo/undo

state只读

  • 只能通过触发action来更新state

  • 集中变更,且以严格顺序发生(没有需要特别小心的竞争条件)

  • action都是纯对象,可以记录日志、序列化,存起来以后还能回放(调试/测试)

reducer都是纯函数

输入stateaction,输出新state。每次都返回新的,不维护(修改)输入的state

所以能随便调整reducer执行顺序,放电影一样的调试控制得以实现

六.react-redux

Redux与React没有任何关系,Redux作为状态管理层可以配合任何UI方案使用,例如backbone、angular、React等等

react-redux用来处理new state -> view的部分,也就是说,新state有了,怎样同步视图?

container

也有containerview的概念(与Flux相同)

container是一种特殊的组件,不含视图逻辑,与store关系紧密。从逻辑功能上看就是通过store.subscribe()读取状态树的一部分,作为props传递给下方的普通组件(view

connect()

一个看起来很神奇的API,主要做3件事:

  • 负责把dispatchstate数据作为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用不可变的数据结构,是出于性能(不可变相关的额外处理)和灵活性(可以配合constimmutablejs等使用)考虑

八.问题与思考

1.state变化订阅机制的粒度控制是怎样的?

subscribe(listener)只能得到全局完整state,那么React setState()粒度是怎样的,怎么分子树?

手动处理。state树有任何变化都通知所有listenerlistener里手动判断自己关注的那一小部分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 - nodechildren变成了childrenIdList,再查总id表得到children

打平能够解决问题,比嵌套状态好维护得多,如果树组件对应一个tree对象的话(node都在tree上),对一棵大树做局部更新会很难受

P.S.3NF竟然能应用在前端,简直难以置信!

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code