少妇白洁系列之React StateUp Pattern, Explained

本文用于阐述StateUp模式的算法和数学背景,以及解释了它为什么是React里最完美的状态管理实现。

关于StateUp模式请参阅:https://segmentfault.com/a/11…

P-State, V-State

如果要做组件的态封装,从组件内部看,存在两种不同的state:

p-state, or persistent state, 是生命周期超过组件本身的state,即使组件从DOM上销毁,这些state仍然需要在组件外部持久化;

v-state, or volatile state, 是生命周期和组件一样的state,如果组件从DOM上销毁,这些state一起销毁;

根据这个定义,React组件的this.state毫无疑问是v-state

开发者常说的model或者store state应该看作p-state,但是这样说过于笼统和宽泛,没有边界;而另一个说法,view state,同样缺乏明确的边界定义;所以我们暂时避免使用这两种表述;用具有严格定义的p-statev-state来展开讨论;

责任与边界

对象封装的责任与边界在面向对象编程里都是特别基础的概念,良好的模块封装必须做到责任明确和边界清晰;每个类型的对象有明确的责任和边界定义,不同类型的对象之间通过组合、接口调用、或者消息机制完成交互,构成易于维护的系统;

但是在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的维护

p-state的实现在StateUp模式中有详细介绍,这里不赘述;这里先阐述一下基于p-statev-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-statev-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

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-statev-state概念,会发现:

  1. 大多数状态管理器把p-state提升到最顶层,构建外部状态树;

  2. 状态管理器需要用户手工代码来实现组件更新绑定,以提高效率,但这是理论上的美好,实际上程序员不会对更新做太细粒度的管理,除非遇到严重性能问题;

  3. 各种状态管理器都在试图利用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,是我们目前已知虽然性能并非最佳,但是解决自动刷新问题的最简单手段;同时函数式编程的易于调试也是巨大的工程收益。

欢迎大家讨论和发表意见。

参考文献

REF 1: https://blogs.msdn.microsoft….

    原文作者:uglee
    原文地址: https://segmentfault.com/a/1190000008592692
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞