React Component裏的狀態機Pattern

State Machine in React Component

React的工程實踐中大多數團隊都只關注了state該怎麼存放的問題,沒有意識到真正致使問題複雜的是組合狀態機,後面這句話對於UI而言是放之四海皆準的;javascript

一個React Component對象做爲UI層元素,在不少狀況下咱們並不但願在狀態遷移時建立新的實例替代舊的,這直接意味着UI組件和狀態機之間是binding關係而不是composition,因此React提供了一個this.state用於解耦,這是它很聰明的一個設計;可是這個this.state只有值成員,沒有方法成員;這意味着寫在Component上的方法裏面要switch/case狀態,這很是不方便。前端

其次React Component的setState方法是merge邏輯而不是replace邏輯,它意味着state下一級props之間必須是平行子狀態機而不是單一狀態機互斥狀態(除非你只有一個狀態機,其餘狀態用值表示);或者換句話說,若是你把不一樣的互斥狀態下的資源和值都放在一個籃子裏時,你每次本身去手動倒空舊的,這一點是個坑。java

第三,那些early binding語言的狀態機Pattern在js和immutable要求下並不適用,他們都是內部值狀態的遷移而不是對象自己被替代,而對象自己被替代這個問題製造了一個問題,就是該對象的方法並不能用於UI的行爲binding,由於狀態遷移後這個舊狀態機對象就廢棄了,調用它的行爲方法固然是不對的;node

解決這個問題並不難,行爲binding使用Component對象上的方法,它是穩定的,不會由於model的狀態機更迭而變化,但它是一個proxy,須要把方法分發到子狀態機上;這樣咱們就獲得了狀態機Pattern的最大優點:每一個狀態只關注屬於本身的子狀態,值,資源,和行爲,不用在全部行爲處理上都狂寫switch/case。程序員

熟悉狀態機Pattern的開發者不難想像出知足上述要求的代碼結構;Component是穩定的,它即便一個子狀態機的容器,又是一個行爲的Proxy層,向this.state下的子狀態機(例如命名爲this.state.stm1)分發行爲;邏輯上是下圖所示:閉包

React Component

  this.state {
    stm1: // --------------------------------> stm1對象
  }

  this.handleToggleButton() {
    this.state.stm1.handleToggleButton() // -> stm1.handleToggleButton()
  }

同時分發的行爲必須返回一個新的狀態機對象用於替代舊的,它可能致使一次狀態遷移,例如方法調用以前this.state.stm1是一個ListViewState對象,而調用後變成了ListEditState對象;若是是這樣,上述行爲方法得加一個邏輯:函數

this.handleToggleButton() {
    let newStm1 = this.state.stm1.handleToggleButton()
    if (newStm1)
      this.setState({ stm1: newStm1 })
  }

這個邏輯會反覆使用,咱們不妨把它抽象出來性能

this.dispatch = (name, method, ...args) => {
    if (this.state[name] &&
      typeof this.state[name] === 'object' &&
      typeof this.state[name][method] === 'function') {
      let next = this.state[name][method](...args)
      if (next) {
        let obj = {}
        obj[name] = next
        this.setState(obj)
      }
    }
  }

這樣在控件的JSX代碼中使用時:ui

onToggle={e => this.dispatch('stm1', 'handleToggleButton')}

這不是惟一的寫法,也許你不喜歡這樣把全部的fallback都處理掉連錯誤通知也沒有;你能夠本身添加,寫成本身喜歡的方式。this

Immutable State Machine in JavaScript

剩下的問題回到如何在JS下書寫一個immutable的狀態機問題,基於Class仍然是直覺的方式,不一樣之處在於狀態遷移時是用舊的Class對象做爲參數傳遞給新的Class對象,新對象的構造函數第一件事情是複製舊對象的所有自有屬性,這個行爲能夠寫在原型類的構造函數裏。

較爲簡潔的寫法是狀態機本身實現一個setState方法(setState是狀態機Pattern的iconic方法,其次纔是entry/exit);該方法只是用於狀態機本身的狀態遷移,和它的容器對象(React Component對象)上的setState方法無關;不要搞混了。(固然你應該想一想爲何React Component上有這個狀態機Pattern裏的標誌性方法)

簡明實現的關鍵點是setState接受兩個參數,第一個是下一狀態的Class名(即構造函數),第二個是...args用於傳參;全部子狀態機的constructor都是(obj, ...args)的形式,obj是上一狀態機;這樣寫能夠避免實現setState時寫switch/case。

它的簡單實現能夠是:

setState(NextState, ...args) {
  // 當前狀態機遷出
  this.exit()
  // 構造新對象,immutable,同時下一狀態機遷入,
  return new NextState(this, ...args)
}

原型類的構造函數能夠看起來這樣:

constructor(obj) {
  Object.assign(this, obj)
}

用於複製上一狀態的全部屬性。

最後這個狀態機的基類須要一個exit方法,若是子類不須要實現,這是個fallback。

綜上所述這個基類看起來大概是這樣:

class STM {

  constructor(obj) {
    Object.assign(this, obj)
  }

  setState(NextState, ...args) {
    this.exit()
    return new NextState(this, ...args)
  }

  exit() {}
}

在實際使用的時候你可能須要本身的基類,由於

  • 你須要一些context,對全部狀態都須要的值、屬性、資源等

  • 你須要一些共同的方法,若是對某個行爲的處理大部分狀態都是同樣的,那麼能夠寫在這個原型類裏,具體某個狀態的行爲不一樣,它能夠去重載;因此一個真正的原型類和繼承類多是這樣的:

class MySTM extends STM {

  constructor(obj) {
    super(obj)
  }

  this.handleToggleButton = () => {
    // ...
  }
}

class MySTMInitState extends MySTM {
  // ...
}

class MySTMAnotherState extends MySTM {
  // ...
}

須要注意的是不要在MySTM的構造函數裏寫其餘邏輯,若是有其餘邏輯,寫在React Component的constructor裏,至關因而這個狀態機原型對象的工廠。

在React Component的構造函數裏,能夠這樣使用:

// 若是props和進入時的上下文有關,在這裏處理
  let props = {
    ...
  }

  // 建立了一個原型
  let stm1 = new MySTMInitState(props)

這裏有兩個問題須要闡述一下。

第一,基於class語法構造對象的本質,其實只是在子類構造函數裏把父類構造函數所有調一遍,保證對象屬性完整,以及原型鏈正確;它是用起來最簡潔的方式,但不是惟一的方式;

JavaScript提供了另外一種方式來構造對象,即Object.create()方法,二者是有區別的。

基於class語法構造的對象,若是你嘗試:

let x = new MySTMInitState({})
let y = new MySTMAnotherState({})

console.log(x.__proto__ === y.__proto__)

你會獲得一個false輸出,即這兩個狀態機的原型對象並不是同一個對象,他們只是同一個構造函數(MySTM)構造過,所以具備一樣的properties(方法)。

可是若是你使用Object.create()來本身構造原型鏈,你能夠有一個原型對象和React Component的生命週期一致,全部stm1狀態機都以它爲原型。這在某些狀況下是有益的,例如:

  1. 你能夠在這個原型上放context,減小遷移時Object.assign()複製properties的性能負擔;

  2. 若是某些context是須要被子類修改的,能夠提供setter方法達到這個目的。

事實上,這個方式更加符合JavaScript的原型化繼承的設計初衷,可是語言是這樣的一個東西,就是哪一個語法簡單,那個寫法就被最普遍的使用,就像C++/Java裏繼承是最簡單的語法,那麼它就被用的最普遍,而寫Pattern是複雜實現,他就被用的少,即便不少時候更應該寫Pattern。

Anyway,這個區別在實踐上的意義很小。

第二,是個對傳統OO語言開發者來講比較難接受的地方,就是你能夠這樣寫:

let x = new MySTM()
let y = new MySTMInitState(x)

這件事情幽默的地方是你能夠用基類對象去構造繼承類對象,彷彿Class和Object的區別被抹平的,他們在平行世界之間穿越。

其實這正解釋了JavaScript的所謂類,只是構造函數,所謂繼承,就是把構造函數和原型對象串起來而已,相似Builder Pattern的思想;因此Build兩步仍是三步都是可能的。

這樣寫有一點實踐上的意義,你能夠先建立一個基類對象初始化全部的上下文,而後根據實際狀況用它來構造繼承類對象,這樣能重用一下繼承類對象的enter邏輯(即constructor),不用重寫。

OK,這兩個都是小問題,細節。move on。

在全部子類中,constructor等價於狀態機Pattern的enter,用於建立全部資源,而exit中須要銷燬全部資源,尤爲是那些出發但還沒有完成的請求,以及還沒有fire的timer。對付這種問題,狀態機是第一首選Pattern,簡直太容易寫出行爲複雜且健壯的代碼了。

事實上,任何其餘形態的維護態的代碼均可以看做是狀態機Pattern的退化,因此對那些若是一開始就預見到將來會變得複雜的組件,應該一開始就寫狀態機;狀態機犧牲的是代碼量,可是對於行爲定義的變化(遷移路徑的增長,減小,改變,狀態增減),它維護起來是無出其右的,是對付複雜多態行爲的首選。

本質上,狀態機幫你拿掉在全部方法裏的第一層switch/case,代之以dispatch,或者是OO裏說的多態;可是若是狀態層疊呢?

一般咱們不在狀態機裏套狀態機,通常只有在寫複雜協議棧的時候這麼寫;通常而言,狀態機兩層最多了,內層的狀態用值來表示狀態,而不是用類來表示狀態,足夠了。

舉個例子看看你理解了沒有:

你的UI裏有一個行爲是操做一個列表中的單一對象;若是有一個對象被選中,而後按鈕被點擊,這是一種行爲,另外一種是用戶先建立一個新對象,這是另外一種行爲;那麼須要把Editing和EditingNew做爲兩種互斥狀態處理嗎?

若是沒有UI的顛覆性變化大多數狀況不這樣作,而是把Editing做爲頂層狀態機(superstate)處理,而New能夠用一個props的值來表示,例如狀態機對象裏有一個叫作creating的prop,它是boolean類型。即頂層狀態機用類對象表示,底層狀態機回到土辦法,用值表示。

這樣設計的好處是:

  1. Editing和EditingNew有大量狀態是重用的和persistent的,即從一個遷移到另外一個,他們仍然是有效的,不該該被一個exit銷燬,另外一個enter重建。

  2. 他們做爲父子狀態設計能夠共用大量方法,而不是每一個都提供本身的副本;

  3. 若是從父狀態遷出或者從外部狀態向父狀態遷入,銷燬和構建資源的邏輯也大部分是相同的;

實際上的狀態圖上每每是有superstate(父狀態)遷出的事件邏輯;那麼執行方式是

  1. 直接調用父狀態的exit

  2. 父狀態的exit先dispatch子狀態的exit

  3. 父狀態的exit再調用本身的邏輯,即清理子狀態的共享資源。

若是是外部遷入父狀態機,要有一個決策依據決定應該遷向那個子狀態機做爲初始狀態,由於在runtime,組合狀態機構成的tree結構,實際的狀態機實例只能在leaf node上,superstate節點的存在是爲了抽象子節點的共同行爲,減小遷移路徑和重用行爲邏輯;

所以遷入父狀態機時(enter)的邏輯和遷出(exit)恰好相反:

  1. 直接調用父狀態機的enter

  2. 父狀態機先構造對全部子狀態都適用的資源

  3. 調用具體某個子狀態機的enter(就是一個if / then來區分子狀態機便可)

在OO領域,不少開發者信奉UML圖;UML圖對OO語言中最重要的類圖,在JavaScript裏毛用沒有了,可是State Machine圖,結合上述狀態機設計,絕對是對付複雜UI的利器;尤爲是對於初學者而言,在前端的狀態邏輯上,你能掌握這一把刀就能砍倒全部的樹;若是還不能砍倒,那其實問題自己不是UI構建域的,多是其餘問題,例如調度等等。

不少寫JavaScript的朋友,爲了向世人證實本身根骨奇佳、習得真傳,處處宣揚OO裏的種種不是,以各類言辭抨擊OO實踐的方方面面。

他們不懂OO。

OO裏在語言層面可能有一些設計問題,可是OO裏的封裝思想是絕對正確的;

爲何會有對象這個概念被提出來?就是由於一些態的生命週期超過函數調用的執行時間,你須要一種方式來管理這些態。

封裝的本質是:在內部有一個state space,在外部看,只看到內部的state space的superstate。物理學上稱之爲簡併,degeneration。

這是咱們對付全部複雜狀態的惟一手段,無論態放在花盆裏、銀行裏、仍是藏在本身的內褲裏,他們都是客觀存在,你不可能去消滅態,你只能organize他們;並且你同時須要organize應用在態上過程(function)。

狀態機把這個organization完徹底全一覽無遺的展露出來,不管你用class寫,用閉包寫,用c語言寫,行爲和狀態的structure都不會變,想成爲一個合格的程序員,尤爲是寫ui的程序員,state machine pattern是必修課。

~~~~~~~~~~~~~~~~~~~

先寫這麼多,我得按照上述邏輯扣代碼去了。

祝你們聖誕節快樂。

歡迎探討。

相關文章
相關標籤/搜索