「換句話說,StateUp模式把面向對象的設計方法應用到了狀態對象的管理上,在遵循React的組件化機制和基於props實現組件通信方式的前提之下作到了這一點。」 ---- 少婦白潔javascript
閱讀本文以前,請肯定你讀過React的官方文檔中關於Lifting State Up的論述:html
https://facebook.github.io/re...java
昨天寫的雛形通過refine以後,獲得了React有史以來最激動人心的代碼模式之一。react
咱們的出發點很簡單:git
但願把有態組件的態提高到父組件管理,這個過程應該能夠向上遞歸,即狀態能夠層層提高;github
在代碼形式層面,應該和原生React組件越兼容越好;segmentfault
現有的React有態組件改爲狀態提高組件(StateUp
)應該很簡單,反之亦然;StateUp
組件有時可能會須要象普通React組件那樣使用;一個最初沒有子組件的StateUp
組件,可能會加入StateUp
子組件;在這些設計變動發生時,組件的修改和組合都應該很簡單,且靈活。數組
咱們首先考慮子組件,子組件的約定是:函數
1 繼承自React.PureComponent
組件化
這個新出現不久的便捷組件在scu時自動作shallow equal的比較,省去本身寫代碼的麻煩;
2 只需修改this.state
和this.setState
爲this.props.state
和this.props.setState
這樣切換代碼模式時很是簡單;this.props.setState
的語義實現和React組件原有的this.setState
一致,即merge狀態對象而不是replace;
3 原來寫在this.state
內的對象,成爲獨立的JavaScript類對象;設計爲類對象而不是Plain Object的好處是它能夠有方法,便於重用;這個類確定和組件類成對使用,因此不妨把它直接嵌入成爲組件類的static成員,統一命名爲State
;
即每一個StateUp
組件看起來這樣:
class MyStateUp extends PureComponent { static State = class State { constructor() { this.something = ... } } }
寫在
class
關鍵字以後的State
不是必須的,可是給這個類賦一個類名的好處是在實現類方法時能夠直接調用類構造函數建立新對象,不然這個構造函數沒有名字。
父組件須要經過props
向子組件傳遞兩個東西,第一個是子組件的state對象,第二個是子組件須要的setState方法;目的是能夠大概寫成:
<SubComponent state={...} setState={...} />
前者比較容易實現,後者有一點小麻煩;
通常咱們向子組件傳遞方法時都是用bound function,綁定父組件的this;若是直接寫在render方法的JSX語法內,每次建立的bound function對象實例是不一樣的,這致使每次父組件render,子組件都要從新render,這不是咱們想要的結果;因此咱們須要一個在父組件上持久化的對象成員提供這個bound function。通常這種狀況會在類的構造函數內建立一個屬性,引用bound function或詞法域bind this的arrow function,但後面會看到咱們有更好的辦法,避免這種手工代碼。
確切的說,這裏說的父組件或者子組件指的是相對角色(role);角色和組件是否爲StateUp
組件無關;一個StateUp
組件能夠是其餘StateUp
組件的父組件,同時也是另外一個StateUp
組件的子組件;即StateUp
組件是能夠組合的。
做爲「子」的責任是前面說的提供內嵌static Class用於構造原有this.state
對象,以及繼承自PureComponent
;
做爲「父」的責任是組合子對象的狀態,同時具備一些類方法,能夠向子組件提供類對象和類對象更新方法;
若是一個組件兼具二者,它能夠繼續向上組合;若是對象只具備後者特性,它是一個普通的React組件,但能夠內部使用StateUp
組件。
StateUp
mixinStateUp
mixin能夠賦予一個StateUp
組件,或者普通React有態組件,成爲「父」組件所需的類方法。
StateUp
的代碼很是簡單,實際上它是一個純函數,向一個React組件內混入(mixin)三個方法:
setSubState 更新某個子組件的狀態對象;父組件能夠直接調用這個方法;
setSubStateBound 提供一個穩定的bound function,傳遞給子組件,子組件能夠用來更新託管的狀態對象;
stateBinding 返回一個props對象,結合spread operator使書寫JSX更方便;
StateUp
mixin自己並不依賴React。
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) } } }
父組件和子組件的狀態約定:父組件使用一個object來做爲狀態容器,其中一個property對應一個子組件,該property引用的對象就是子組件的狀態對象;全部狀態對象都是class對象;這些對象構成的樹,就是StateUp
組件經過組合構成的樹,不一樣之處在於對象的生命週期是超過對應組件的生命週期的;在JSX語法中常常根據須要顯示或刪除某個組件,但該組件對應的狀態對象,能夠在父組件的狀態對象內持久;
setSubState
方法是用於更新子組件狀態的方法;
setSubState
的第一個參數是(property) name,第二個參數是子組件提供的nextState,按照React習慣,它是partial state,應merge到子組件的狀態中;
setSubState
代碼第一句拿到父組件的state,若是父組件是有態的就是this.state
,若是父組件的態託管到更高的組件上去,就是this.props.state
;
第二句拿到子組件的state;
第三句先構造一個新的子組件狀態對象,注意它是new
出來的,不是{}
;而後子組件的當前狀態和新的狀態都merge進去,獲得父組件的nextState對象(也是partial state);
最後一句更新狀態,若是父組件也是StateUp
組件,它繼續調用父父組件方法更新狀態,若是不是,它直接調用React組件的setState方法,二者的語義被設計成一致的;
本質上這個函數是一個遞歸函數;React的this.setState
最終必定會被調用,但在此以前,一直是this.props.setState
在沿着StateUp
組件的層級關係自下向上調用;
若是熟悉immutable的話,會發現StateUp
組件的狀態對象樹是知足immutable要求的,由於在計算nextSubStateMerged時使用了Object.assign()
;因此這個遞歸過程沿着對象路徑一直到root都會更新,因爲全部StateUp
組件都是繼承自PureComponent,自帶的SCU,其結果是:
按照React的設計邏輯,這是最佳性能;
PureComponent自帶SCU邏輯,不須要手工代碼;
沒有額外的state store, dispatch, action/event之類的邏輯,只使用React組件和JavaScript類對象來維護狀態;
真正會觸發render的setState
只在頂級父組件中被調用一次;不是組件和狀態管理器之間binding以後的觸發多個組件的render。
setSubStateBound
用於產生和獲取父組件向子組件傳遞的setState
bound function
它首先在父組件上建立名字爲setSubStateBoundObj
的容器,用於裝載bound function;通常使用而言不必用Map,就用Plain Object作key value容器便可;
該函數採用了Lazy方式工做,即每次須要時才建立bound function;這個作法優於試圖在構造函數中建立這些bound function的作法,後者或者須要用戶手工代碼,或者須要使用Decorator去Hack class的構造函數,沒有必要的複雜、危險、或不兼容;
該函數爲每一個子組件建立一個bound function(即子組件的this.props.setState
);它除了bind this以外還須要bind (property) name;建立的bound function保存在容器內,每次調用setSubStateBound
時返回結果一致,確保了全部子組件的SCU工做。
最後的stateBinding
方法是便利函數,用於書寫JSX時不須要手寫state
和setState
,使用spread operator便可;
<SubComponent {...this.stateBinding('subComponentName')} />
StateUp
的核心是實現了組件(view)和它的狀態對象(state)的解耦;
StateUp
組件的狀態對象是class對象,這個class是JavaScript class,與React無關,它能夠提供各類方法;
其中的只讀方法,對父組件而言能夠直接訪問,這對於父組件協調幾種子組件的顯示很是有用,例如按鈕的使能;
class對象也能夠有改變對象狀態的方法,但約定是,它只能返回新對象,即保證immutable;對於子組件而言,這些方法能夠直接調用,而後經過this.props.setState
實現更新;對於父組件而言這些方法一樣能夠調用,但更新路徑是this.setSubState
;
後者至關於在普通的React組件組合中,父組件在拿到子組件的引用後直接去調用子組件的setState
方法;
在React中這不是值得推薦的方法(洗剪吹們稱之爲anti-pattern),首先是由於React的setState
沒有封裝可言,調用該方法須要理解組件內部的狀態的含義,其次子組件很動態,父組件容易拿到舊的子組件的引用致使錯誤;
StateUp
組件的狀態對象從組件中剝離出去,它解耦了「須要更新狀態以更新顯示」和「瞭解如何更新狀態」這兩件事情,後者用類對象方法實現封裝,而父組件能夠只完成前者;
例如一個輸入框對應的狀態對象可能有一個叫作reset
的方法,父組件能夠調用reset
方法得到一個新的子組件狀態對象,父組件僅更新對象便可,它不須要了解reset
如何工做;reset
方法寫在狀態對象類上,自己也能夠最大限度的重用;
另一個例子,考慮一個Form和嚮導組件;Form中的一些元素,例如用戶的用戶名和密碼輸入框,若是輸入合法,則next button使能;
在StateUp
模式中,用戶名密碼輸入能夠做爲一個組件封裝,它的狀態對象能夠提供ready方法用於判斷是否完成,這比在組件上提供props和傳遞bound function通知方便,父組件也不須要cache一個ready狀態;
一樣的,若是next以後,用戶名密碼組件從視圖中移除,父組件須要保存以前輸入的用戶名密碼副本,若是用戶回退,這些內容還要交給新建立的用戶名密碼組件,而在StateUp
模式下,這些都不是問題,由於子組件狀態對象並未消除,它也不須要把內容展開到父組件的狀態容器內,若是父組件須要獲取輸入結果,那麼一個get方法便可作到;
這樣的組件不管在嚮導頁面、用戶修改用戶名密碼的頁面等等地方都很容易重用,父組件僅僅須要在本身的狀態內建立一個子組件的狀態對象便可,在render時也僅僅須要傳遞這個對象而不是展開的一組props,也不須要去增長不少接受狀態變化通知的方法並傳遞到子組件上;
換句話說,StateUp
模式把面向對象的設計方法應用到了狀態對象的管理上,在遵循React的組件化機制和基於props實現組件通信方式的前提(context)之下作到了這一點。
可以在組件的狀態對象上實現維護狀態的方法,父子組件都可訪問,均有更新路徑,是StateUp
模式的重要的收益,它兼顧了便利性、靈活性、和代碼重用。
下面看一個簡單且無聊的代碼實例:
class Sub extends PureComponent { static State = class State { constructor() { this.label = '' } } render() { console.log('Sub render:' + this.props.state.label) return ( <div> <button style={{width: 64, height: 24}} onClick={() => this.props.setState({ label: this.props.state.label + 'a' })} > {this.props.state.label} </button> </div> ) } }
能夠看到對子組件而言沒有由於Pattern引入帶來的過多代碼負擔;StateUp
組件須要提供狀態對象的class,必須寫成static
且名字爲State
;這是一個約定;父組件利用這個約定找到子組件的構造函數建立子組件的狀態對象;
下面的代碼展現瞭如何在父組件中使用子組件;這個父組件自己也是一個StateUp
組件,即繼續向上層容器傳遞狀態,而不是本身維護狀態;
class Composite extends StateUp(PureComponent) { static State = class State { constructor() { this.sub1 = new Sub.State() this.sub2 = new Sub.State() this.sub3 = new Sub.State() } } render() { return ( <div> <Sub {...this.stateBinding('sub1') } /> <Sub {...this.stateBinding('sub2') } /> <Sub {...this.stateBinding('sub3') } /> </div> ) } }
注意extends關鍵字後面的寫法,這是目前爲止JavaScript裏最好的mixin模式,它不污染prototype,也沒有由於前衛的語法致使兼容性問題,StateUp
自己不重載constructor,也不會影響super,instanceof等關鍵字的使用;
Composite
也是StateUp
組件,也要提供一個State class,其構造函數中調用Sub.State類的構造函數構造子組件的狀態對象,用sub1
, sub2
,sub3
命名;
render方法裏展現了子組件的使用方式;這裏應該看做是一種binding;把一個組件的狀態對象和它的view binding在一塊兒,後者是pure的。
Composite
仍然是StateUp
組件,這意味着若是要使用它須要一個更上層的容器;咱們來寫一個通用的組件終結這個層層向上傳遞狀態的遊戲。
class Stateful extends StateUp(Component) { constructor(props) { super() this.state = { state: new props.component.State() } } render() { let Component = this.props.component return <Component {...this.stateBinding('state')} /> } } class App extends Component { render() { return <Stateful component={Composite} /> } }
Stateful
仍然須要繼承StateUp
mixin,這樣它就有在內部組合使用StateUp
組件的便利;可是它不用PureComponent作起點,而使用標準的React Component,它是有態的,也是它下面全部StateUp
組件樹的惟一頂層態容器。
Stateful
不須要static State class,它直接用本身的this.state做爲狀態容器;因爲StateUp
代碼裏要求子組件狀態對象在父組件狀態對象中必須有名字,因此這裏在this.state
內再建立一個叫state的property,引用子組件狀態對象(這樣能夠重用代碼);
Stateful
是通用的,它具體wrap了哪一個StateUp
組件,用名字爲component
的prop傳遞進來,在render方法裏直接渲染這個component便可;
最終咱們在示例代碼中用Stateful把Composite用在頁面上。
上述代碼很容易調試;在Sub組件的render方法中有一句打印,能夠看到在每次點擊button時只有該button會渲染,即全部StateUp
,做爲PureComponent,SCU自動工做;
目前的代碼只能用對象方式組合,不能用數組,但這不是一個很大的麻煩,若是你仔細看StateUp
mixin函數代碼就會發現,name改爲index是很容易的,只是bound function的處理方式要當心,由於它在對象被銷燬以前沒有回收機制。
這個Pattern不是爲了做爲大一統的狀態管理器被提出的;我最初只想實現一些反覆重寫的代碼的重用;
React自己經過Composition的重用,在理論上沒有問題,但很是不靈活;雖然有container component和pure component的概念,可是container component的狀態變化,仍然須要在更高層的組件內cache狀態,cache的更新經過props傳遞notification實現,這造成了一個兩難局面:若是狀態local,則組件的props設計須要考慮可能的觀察者邏輯,若是狀態提高,則破壞封裝原則;
StateUp
模式就是爲了解決這個問題設計的;它給出了一種方式讓子組件既能獨立封裝邏輯,便於重用,又能繞開寫起來很是繁瑣的props通信機制,讓父組件方便獲取子組件狀態,靈活組合行爲;
StateUp
組件的重用能力是卓越的,你不須要把狀態和維護狀態的邏輯代碼放到另一個文件裏;StateUp
也沒有外部依賴,不強制要求消息總線或狀態管理器,沒有所以致使的性能問題,binding問題,消息名稱的namespace問題;它是百分之百純JS和百分之百純React;
它在性能上,以及爲了獲取這種性能所須要的額外編碼上,也接近完美。
事實上,我我的認爲,既然React都有了PureComponent做爲內置組件,這種StateUp
模式,也應該是React內置功能。
最新關於React StateUp模式的數學背景介紹: https://segmentfault.com/a/11...
subProps
函數重命名爲stateBinding
,由於本質上它是向子組件綁定狀態和更新狀態的方法;