本文用于阐述StateUp
模式的算法和数学背景,以及解释了它为何是React里最完美的状态管理实现。javascript
关于StateUp
模式请参阅:https://segmentfault.com/a/11...java
若是要作组件的态封装,从组件内部看,存在两种不一样的state:程序员
p-state
, or persistent state, 是生命周期超过组件自己的state,即便组件从DOM上销毁,这些state仍然须要在组件外部持久化;算法
v-state
, or volatile state, 是生命周期和组件同样的state,若是组件从DOM上销毁,这些state一块儿销毁;编程
根据这个定义,React组件的this.state
毫无疑问是v-state
;redux
开发者常说的model
或者store state
应该看做p-state
,可是这样说过于笼统和宽泛,没有边界;而另外一个说法,view state
,一样缺少明确的边界定义;因此咱们暂时避免使用这两种表述;用具备严格定义的p-state
和v-state
来展开讨论;segmentfault
对象封装的责任与边界在面向对象编程里都是特别基础的概念,良好的模块封装必须作到责任明确和边界清晰;每一个类型的对象有明确的责任和边界定义,不一样类型的对象之间经过组合、接口调用、或者消息机制完成交互,构成易于维护的系统;性能优化
可是在React里,这个设计方式变得难以付诸实施;数据结构
React的机制是父组件不应直接访问子组件,由于子组件的生命周期不是父组件维护的,是React维护的;React父组件不去访问子组件也意味着子组件须要的状态要提高至父组件维护,父组件更新了这些状态以后,经过props
向下传递;框架
触发状态改变的缘由能够由子组件发起(看起来更像封装),可是须要父组件提供Callback,逻辑处理仍然由父组件完成;这意味着子组件的状态和行为,都托管到父组件去了,子组件只负责渲染和解释用户输入行为;但这给封装和重用制造了麻烦,相同的逻辑会重复书写在不一样的父组件中;
在StateUp
模式中,咱们明确给出了p-state
的定义和实现,即StateUp
组件中的静态State类,用于构造p-state
对象;
StateUp
组件的渲染函数至关于f(p-state, v-state, props)
;
增长的p-state
对象用于维护本来要提高至React父组件的状态,以及行为;换句话说,若是使用p-state
对象,本来由子组件托管到父组件维护的(属于子组件维护责任的)状态,及其致使的经过props
向下传递的数据,应该移动到p-state
内维护,组件直接经过this.props.state
访问;
固然这不能消除一个StateUp
组件渲染须要的全部props
,因为HTML/DOM的结构设计,完整渲染组件须要的数据注定是它的全部父组件容器向下传递的数据的总和的一部分(即须要用到的部分)。
p-state
的实如今StateUp
模式中有详细介绍,这里不赘述;这里先阐述一下基于p-state
和v-state
概念,StateUp
模式中的生命周期问题如何严格表述,而后阐述StateUp
模式的数学本质;
在StateUp
模式中,StateUp
组件A的p-state
不在组件A中维护,它须要提高至父组件B,提高有多是递归的,即在父组件B中被继续提高;直到某一个React组件C,把这个树状层级的p-state
对象放置在本身的v-state
(this.state
)中,这意味着StateUp
组件A的状态生命周期,和组件C的视图生命周期是一致的;
咱们把组件C称为组件A的p-state ancestor
;
组件C在它的任何子组件的p-state
发生变化时,都会调用this.setState
更新本身的v-state
,对于React而言,这触发全部子组件的渲染;但因为immutable的数据结构和PureComponent的SCU设计,render是按需的,仅须要render的子组件会被render;
p-state
的更新路径在StateUp
模式中有一些一眼看上彷佛不合理的设计;
const StateUp = base => class extends base { setSubState(name, nextSubState) { let state = this.props.state || this.state let subState = state[name] let nextSubStateMerged = Object.assign(new subState.constructor(), subState, nextSubState) let nextState = { [name]: nextSubStateMerged } this.props.setState ? this.props.setState(nextState) : this.setState(nextState) } setSubStateBound(name) { let obj = this.setSubStateBoundObj || (this.setSubStateBoundObj = {}) return obj[name] ? obj[name] : (obj[name] = this.setSubState.bind(this, name)) } stateBinding(name) { return { state: this.props.state ? this.props.state[name] : this.state[name], setState: this.setSubStateBound(name) } } }
既然咱们明确分清了p-state
和v-state
,为何p-state
的更新,要象上述代码同样走React组件的方法,为何不是把p-state
对象单独构建一个tree,毕竟它是JavaScript对象,写起来并不难;
这个问题的本质涉及到了immutable的tree数据结构的一个常见问题,即你不可能构建一个cyclic数据结构是immutable的,至少在JavaScript这种有statement没有lazy evaluation的语言里不可能;
事实上我为这个想法写了代码,例如在父组件的p-state
对象中这样写:
// parent component p-state object static State = class State { constructor() { this.sub1 = new Sub.State() this.sub1.parent = this this.sub1.propName = 'sub1' } }
这样就在子组件的p-state
内装载了父组件的p-state
的引用;看起来在子组件的p-state
上彷佛能够设计一个setState
方法(不是React Component上的setState),直接调用父组件的p-state
对象上的setState
方法,就能够实现递归更新;
但这是一个假象;考虑以下A/B/C/D的结构:
A -> A' B B' C C' D D
在C更新至C'时,D没有变化,可是D的父对象再也不是B而是B';
解决这个问题的办法,也是通用的immutable tree数据结构的双向引用问题的解法,是所谓的Red-Green Tree (参见参考文献)。
Red-Green Tree在外部看是一个Tree,在内部分红Red Tree和Green Tree,外部访问经过Red Tree,Green Tree是内部的;
Red Tree的结构和Green Tree如出一辙,它是一个mutable tree,每一个节点包含自下至上的引用(parent引用)和向右引用Green Tree上的对应对象,Green Tree是immutable tree,只有自上至下的引用:
red tree green tree A -> null, A' A' B -> A, B' B' C -> B, C' C' D -> B, D' D' A -> null, A" A" B -> A, B" B" C -> B, C" C" D -> B, D' D'
当操做C的时候,Green Tree的A'/B'/C'都会发生变化,同时Red Tree自上至下更新,它的向上引用不变,可是向右的引用所有刷新成最新的Green Tree对象;这样既维护了双向引用,又实现了immutable;
在StateUp
模式中,Component至关于Red Tree上的节点,p-state
对象是Green Tree上的节点;Component的this.prop.state
至关于向右引用,render时自上至下更新(B' -> B");this.prop.setState
至关于向上引用(B->A),它是稳定的,这个稳定引用保证在更新D时能够先找到父节点B而后找到最新的B",从而正确实现D在父对象里的引用更新;
因为React自上至下渲染,因此在父组件内拿子组件的引用是危险的,由于可能过时;可是子组件向父组件的引用在每次渲染以后都是保证正确的;
因此在StateUp
模式中,经过用Component承担Red Tree的责任,保证p-state tree
能够实现immutable的Green Tree,有此带来p-state
对象的高可维护性和性能保证;
我曾经认为stateBinding
函数实现了两个prop传递是不太合理的设计,但从上面的图示看这很是合理,其中state
是子组件的向右引用,setState
是子组件的向上引用,利用React的props和render机制实现Red-Green Tree的更新,这是React和Immutable的完美结合。
若是去对比其余的React状态管理器,使用这里给出的p-state
和v-state
概念,会发现:
大多数状态管理器把p-state
提高到最顶层,构建外部状态树;
状态管理器须要用户手工代码来实现组件更新绑定,以提升效率,但这是理论上的美好,实际上程序员不会对更新作太细粒度的管理,除非遇到严重性能问题;
各类状态管理器都在试图利用immutable来作性能优化,可是没有触及问题的本质,即Red-Green Tree问题,这也是React的本质;若是你仅仅使用全局状态树,你只作对了问题的一半。
相信每一个深刻思考过React的外部状态树和组件树关系的程序员都曾经在大脑中有过这样的问题,它们两个到底该不应一致?
StateUp
模式给这个问题一个明确的回答:应该,但不是在React组件层面上的,而是StateUp
组件层面的;更确切的说是p-state ancestor
组件构成的树,就是Model的结构树,它包含运行时组件状态组合结构和生命周期两方面的定义;而每一个节点拓扑展开的React Component子树,仅具备视图层的含义;
因此在设计时仔细考虑p-state ancestor
的处理,是对状态该写在哪里的最有帮助的思考;同时,基于StateUp
模式,这个Model的结构是自动组出来的,不是开发者独立定义的;
React的this.state
仅针对v-state
设计,在没有p-state
对象封装的状况下,它至关于把p-state ancestor
的子树展开后,内部全部形式无态但本该有态的组件的态和行为都提高到该组件内实现,为代码重用和维护带来很大麻烦;
从前面Red-Green Tree的分析能够看出,提取p-state
进行对象封装,不可是可行的,并且是恰当的,能够有效利用PureComponent特性提升最高渲染效率,在模型上也有数学算法的支撑。
由于工做繁忙我无心把StateUp
模式搞成象redux
那样的流行项目,代码量也撑不起一个项目的规模;并且StateUp
的代码自己也还显得过于简陋,也许让p-state
对象可以emit event能够创造更多的便利,等等;
可是在工程实践上我会积极实践这种方式,遇到实际问题也会尽最大努力去在这个框架下寻求解决方案,毕竟在目前阶段看起来,StateUp
模式把UI的开发带回了咱们熟悉的面向对象领域,对各类复杂的行为模式和结构模式,都有大量的成熟模式可用,而没必要在很是割裂的组件交互机制下感受捉襟见肘。
这20行代码是我一年多的React开发实践中写过的最好的代码,它很粗糙,可是它背后的算法模型有异常简单强大的力量;
它并非基于Red-Green Tree推演的结果,而是偶得后对其作更深层面的思考,发现它彻底契合了immutable数据结构和函数式编程的设计思想;而immutable,是咱们目前已知虽然性能并不是最佳,可是解决自动刷新问题的最简单手段;同时函数式编程的易于调试也是巨大的工程收益。
欢迎你们讨论和发表意见。