Redux概念之二: Redux的三大原則

eyesofkids發表於2017-01-24

Redux裡的強硬規則與設計不少,大部份都會與FP(函式式程式開發)、改進原本的Flux架構設計有關。Redux官網文件上的三大基本原則,主要是因為有可能怕初學者不理解Redux中的一些限制或設計,所以先寫出來說明,這裡面也說明了Redux的設計原理基礎是如何,所以強烈建議所有的初學者一定要徹底地理解這三大原則中的意義,多看幾遍,對日後的學習會很有幫助。以下分別說明,主要以原文的標題與內容說明,儘可以說明的比較清楚些。

單一真相來源

你的整個應用中的state(狀態),會儲存在單一個store(儲存)之中的一個物件樹狀結構裡。

Redux中只有用單一個物件大樹結構來的儲存整個應用的狀態,也就是整個應用中會用到的資料,稱之為store(儲存)。但要注意的是store(儲存)並不是只有單純的資料而已。store就是應用程式領域的狀態,它是型別MVC中的Model(模型的)設計的概念,這設計是由Flux架構而來的,在原本的Flux架構中是允許多個store的結構,Redux簡化為只有單一個。

Redux的單一個store的設計有一些好處,對開發者來說,它可以容易除錯與觀察狀態的變化,狀態儲存於物件樹狀結構中,也很容易作到重作/復原(Undo/Redo)的功能。因為只有一個store,但如果store裡要儲放多個不同的狀態物件,以及每次的更動資料,自然就會變成了物件樹狀結構(object tree)。

此外,如果你想要從store中取出目前的狀態資料,可以用store的getState()方法。

狀態是唯讀的

唯一能更動狀態的是傳送一個action(動作),action是一個描述”發生了什麼事”的純物件

這裡指的”狀態”,是上面說的儲放在store中的狀態資料,你”不能直接”對其中的狀態資料更動,只能”間接”地作這事。這與原先的React中的statesetState的概念有點像,Redux的意思是你不能直接更動store裡面所記錄的狀態值,只能”間接”地透過傳送action物件來叫store更動狀態。”間接”地更動狀態是一個很關鍵的設計,這是Flux中單向資料流的重點之一,這對於每個動作發生,最終會影響到什麼狀態上的更動,一個接一個的順序等等的一種嚴格的設計。

你可能會認為”狀態既然是唯讀”,直接與間接有什麼差異,”唯讀”不就代表完全不能更動,這語句是不是有誤?

“唯讀”當然就是完全不能更動的意思沒錯,所以狀態物件的更動是並不是在原先的狀態物件上變動它,而是由原先狀態物件因為動作的加入後,產生一個全新的狀態物件,用這全新的狀態物件來取代原先的狀態物件而已。這在真實世界中或許很難拿比喻來說明,但在軟體世界中這很可以很容易達成。

“發生了什麼事”這句,是代表每個action都會有一個type(型別),代表這個動作是要作什麼用的,或是現在是發生了什麼,例如是要新建一筆什麼資料,或是刪除整個資料等等,動作物件除了要說明它是要作什麼之外,也需要包含所影響的資料。

傳送一個action(動作),使用的是store.dispatch(action)語法樣式,下面這個例子就是一個要更動狀態的程式碼:

store.dispatch({
  type: `COMPLETE_TODO`,
  index: 1
})

中間的那個純物件,就叫作action(動作),它是一個單純用於描述發生了什麼事與相關資料的純物件:

{
  type: `COMPLETE_TODO`,
  index: 1
}

還記得我們在React中的statesetState方法的設計嗎?state也是不能直接更動的,一定要透過setState方法才能更動它。那這是為什麼呢?因為setState不光只是更動state值,它還要作重新渲染的動作,React需要比對目前的狀態,與即將要變動的狀態,這樣才能進移動新渲染的工作。Redux的設計中store是與React中的state相比,它們之間有一些類似的設計。

更動只能由純函式來進行

要指示狀態樹要如何依actions(動作)來作改變,你需要撰寫純粹的歸納函式(reducers)

Redux中的reducer的原型會長得像下面這樣,你可以把它當作就是 之前的狀態 + 動作 = 新的狀態 的公式:

(previousState, action) => newState

注: 你可以參考Redux中Reducers這一章的內容,裡面有例項。

不過,Redux中的reducer一定是純函式(pure function),也就是不能有副作用的函式。因此由reducer所產生的新狀態,並不是直接修改之前的狀態而來,而是一個複製之前狀態,然後加上動作改變的部份,產生的一個新的物件,它這樣設計是有原因的。

Redux的store設計,並不是原本Flux架構中的store,而是ReduceStore,這個ReduceStore是一個在Flux中的store進化版本,在說明中它有一個叫作reduce的方法,說明如下:

reduce(state: T, action: Object): T 歸納(Reduces)目前的state(狀態)與一個action(動作)到新的store中的state(狀態)。所有的子類都需要實作這個方法。這個方法必須是純粹而是無副作用。

那為何要用這個進化的ReduceStore?它最後有說明一段:

不需要傳送更動事件注意所有繼承自ReduceStore的store,不需要手動傳送在reduce()中的更動事件…state(狀態)會自動地比對在每個dispatch(傳送)之前與之後,與自動地作傳送更動事件…

ReduceStore的設計與Redux最一開始的版本差不多是同時間釋出的,在開發者之間彼此有交流。Redux的store運用了類似於ReduceStore的設計,所以要更動Redux中的store,需要透過reducer,這是為了簡化原本在Flux資料流的實作流程。

reducer在Redux中扮演了十分重要的關鍵角色,它是一種對store中所存放的狀態,要如何因應不同的動作而進行重新整理的函式,而store也是由reducer所建立,例如像下面的程式碼:

// @Reducer
//
// action payload = action.text
// 使用純粹函式的陣列unshift,不能有副作用
// state(狀態)一開始的值是空陣列`state=[]`
function todoApp(state = [], action) {
  switch (action.type) {
    case `ADD_ITEM`:
      return [action.text, ...state]
    default:
      return state
  }
}

// @Store
//
// 由reducer建立store
const store = createStore(todoApp)

針對應用中不同功能的狀態,可以分別寫出不同的reducer,Redux中提供了combineReducers函式可以合併多個reducer,例如以下的程式碼:

function todos(state = [], action) {
  switch (action.type) {
    case `ADD_TODO`:
      return state.concat([ action.text ])
    default:
      return state
  }
}

function counter(state = 0, action) {
  switch (action.type) {
    case `INCREMENT`:
      return state + 1
    case `DECREMENT`:
      return state - 1
    default:
      return state
  }
}

// rootReducer是個組合過的函式,
// 這裡用的是物件屬性初始設定簡寫法,
// combineReducers傳參是一個物件
const rootReducer = combineReducers({
  todos,
  counter
})

相關文章