少婦白潔系列之React StateUp Pattern, Explained

本文用於闡述StateUp模式的算法和數學背景,以及解釋了它爲何是React裏最完美的狀態管理實現。javascript

關於StateUp模式請參閱:https://segmentfault.com/a/11...java

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-stateredux

開發者常說的model或者store state應該看做p-state,可是這樣說過於籠統和寬泛,沒有邊界;而另外一個說法,view state,一樣缺少明確的邊界定義;因此咱們暫時避免使用這兩種表述;用具備嚴格定義的p-statev-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的維護

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....

相關文章
相關標籤/搜索