Redux

2019-06-12 14:52:02 浏览数 (1)

一.作用

与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问题,立即修改生效

四.结构

代码语言: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负责描述发生了什么(就像新闻标题)

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-reduct的是怎么回事?

猜一下。应该是把store挂在hostContainerInfo上了,所以要求在render root时把Provider作为顶层容器:

代码语言:javascript复制
render(
 <Provider store={store}>
   <App />
 </Provider>,
 document.getElementById('root')
)

hostContainerInfo长这样子:

代码语言:javascript复制
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 - nodechildren变成了childrenIdList,再查总id表得到children

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

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

参考资料

  • Redux doc:非常棒的文档,读起来根本停不下来
  • Redux · An Introduction

0 人点赞