摘要:一般這種情況會在類的構造函數內創建一個屬性,引用或詞法域的,但后面會看到我們有更好的辦法,避免這種手工代碼。
“換句話說,StateUp模式把面向對象的設計方法應用到了狀態對象的管理上,在遵循React的組件化機制和基于props實現組件通訊方式的前提之下做到了這一點。” ---- 少婦白潔
閱讀本文之前,請確定你讀過React的官方文檔中關于Lifting State Up的論述:
https://facebook.github.io/re...
昨天寫的雛形經過refine之后,得到了React有史以來最激動人心的代碼模式之一。
我們的出發點很簡單:
希望把有態組件的態提升到父組件管理,這個過程應該可以向上遞歸,即狀態可以層層提升;
在代碼形式層面,應該和原生React組件越兼容越好;
現有的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方法;目的是可以大概寫成:
前者比較容易實現,后者有一點小麻煩;
一般我們向子組件傳遞方法時都是用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即可;
解耦
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 () } }
可以看到對子組件而言沒有因為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 () } }
注意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} } class App extends Component { render() { return } }
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,因為本質上它是向子組件綁定狀態和更新狀態的方法;
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/81905.html
摘要:本文用于闡述模式的算法和數學背景,以及解釋了它為什么是里最完美的狀態管理實現。歡迎大家討論和發表意見。 本文用于闡述StateUp模式的算法和數學背景,以及解釋了它為什么是React里最完美的狀態管理實現。 關于StateUp模式請參閱:https://segmentfault.com/a/11... P-State, V-State 如果要做組件的態封裝,從組件內部看,存在兩種不同的...
摘要:目的是為了解決在重用的時候,持久和方法重用的問題。換句話說你不用擔心把組件寫成模式不好重用,如果你需要傳統的方式使用,一下即可。 這篇文章所述的思想最終進化成了一個簡單的狀態管理模式,稱React StateUp Pattern,詳細介紹請參閱:https://segmentfault.com/a/11... 寫了一個非常簡單的實驗性Pattern,暫且稱為PurifiedCompon...
摘要:匿名函數是我們喜歡的一個重要原因,也是,它們分別消除了很多代碼細節上需要命名變量名或函數名的需要。這個匿名函數內,有更多的操作,根據的結果針對目錄和文件做了不同處理,而且有遞歸。 能和微博上的 @響馬 (fibjs作者)掰扯這個問題是我的榮幸。 事情緣起于知乎上的一個熱貼,諸神都發表了意見: https://www.zhihu.com/questio... 這一篇不是要說明白什么是as...
摘要:的科學定義是或者,它的標志性原語是。能解決一類對語言的實現來說特別無力的狀態機模型流程即狀態。容易實現是需要和的一個重要原因。 前面寫了一篇,寫的很粗,這篇講講一些細節。實際上Fiber/Coroutine vs Async/Await之爭不是一個簡單的continuation如何實現的問題,而是兩個完全不同的problem和solution domain。 Event Model 我...
摘要:上例的功能塊定義了如下節點樹入口節點是面板,結合該節點的函數書寫特點,我們接著介紹最佳實踐如何處理功能塊之內的編程。 本文介紹 React + Shadow Widget 應用于通用 GUI 開發的最佳實踐,只聚焦于典型場景下最優開發方法。分上、下兩篇講解,上篇概述最佳實踐,介紹功能塊劃分。 showImg(https://segmentfault.com/img/bVWu3d?w=6...
閱讀 813·2021-11-18 10:02
閱讀 2503·2021-11-11 16:54
閱讀 2750·2021-09-02 09:45
閱讀 654·2019-08-30 12:52
閱讀 2774·2019-08-29 14:04
閱讀 2745·2019-08-29 12:39
閱讀 448·2019-08-29 12:27
閱讀 1887·2019-08-26 13:23