Redux開發實用教程

CrazyCodeBoy發表於2019-03-09

為了幫助大家快速上手什麼是Redux開發,在這本節中將向大家介紹什麼是Redux開發所需要的一些什麼是Redux必備基礎以及高階知識

什麼是Redux?

Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理,可以讓你構建一致化的應用,執行於不同的環境(客戶端、伺服器、原生應用),並且易於測試。

redux-flow

我們過下整個工作流程:

  1. 使用者(操作View)發出Action,發出方式就用到了dispatch方法;
  2. 然後,Store自動呼叫Reducer,並且傳入兩個引數(當前State和收到的Action),Reducer會返回新的State,如果有Middleware,Store會將當前State和收到的Action傳遞給Middleware,Middleware會呼叫Reducer 然後返回新的State;
  3. State一旦有變化,Store就會呼叫監聽函式,來更新View;

到這兒為止,一次使用者互動流程結束。可以看到,在整個流程中資料都是單向流動的。

Redux和Flux的對比

Redux是Flux思想的一種實現,同時又在其基礎上做了改進。Redux秉承了Flux單向資料流、Store是唯一的資料來源的思想。

  • Redux中沒有Dispatcher:它使用Store的Store.dispatch()方法來把action傳給Store,由於所有的action處理都會經過這個Store.dispatch()方法,所以在Redux中很容易實現Middleware機制。Middleware可以讓你在reducer執行前與執行後進行攔截並插入程式碼,來達到操作action和Store的目的,這樣一來就很容易實現靈活的日誌列印、錯誤收集、API請求、路由等操作。
  • Redux只有一個Store:Flux中允許有多個Store,但是Redux中只允許有一個,相較於多個Store的Flux,一個Store更加清晰,並易於管理;

Redux和Flux的最大不同是Redux沒有 Dispatcher 且不支援多個 store。Redux只有一個單一的 store 和一個根級的 reduce 函式(reducer),隨著應用不斷變大,我們需要將根級的 reducer 拆成多個小的 reducers,分別獨立地操作 state 樹的不同部分,而不是新增新的 stores。

Redux優點

  • 可預測: 始終有一個唯一的準確的資料來源(single source of truth)就是store,通過actions和reducers來保證整個應用狀態同步,做到絕不混亂
  • 易維護: 具備可預測的結果和嚴格的組織結構讓程式碼更容易維護
  • 易測試: 編寫可測試程式碼的首要準則是編寫可以僅做一件事並且獨立的小函式(single responsibility principle),Redux的程式碼幾乎全部都是這樣的函式:短小·純粹·分離

為什麼要用Reudx?

隨著 JavaScript 應用越來越大,越來越複雜,我們需要管理的state變得越來越多。 這些 state 可能包括伺服器響應、快取資料、本地生成尚未持久化到伺服器的資料,也包括 UI 狀態,如啟用的路由,被選中的標籤,是否顯示載入動效或者分頁器等等。

管理不斷變化的 state 非常困難。如果一個 model 的變化會引起另一個 model 變化,那麼當 view 變化時,就可能引起對應 model 以及另一個 model 的變化,依次地,可能會引起另一個 view 的變化。直至你搞不清楚到底發生了什麼。state 在什麼時候,由於什麼原因,如何變化已然不受控制。 當系統變得錯綜複雜的時候,想重現問題或者新增新功能就會變得非常複雜。

雖然React 試圖在檢視層禁止非同步和直接操作 DOM 來解決這個問題。美中不足的是,React 依舊把處理 state 中資料的問題留給了你。Redux就是為了幫你解決這個問題。

Redux 的三個基本原則

  • 單一資料來源:整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中;
  • State 是隻讀的:唯一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通物件;
  • 使用純函式來執行修改:為了描述 action 如何改變 state tree ,你需要編寫 reducers;

Redux有那幾部分構成?

  • action:action就是一個描述發生什麼的物件;
  • reducer:形式為 (state, action) => state 的純函式,功能是根據action 修改state 將其轉變成下一個 state;
  • store:用於儲存state,你可以把它看成一個容器,整個應用只能有一個store。

Redux應用中所有的 state 都以一個物件樹的形式儲存在一個單一的 store 中。 惟一改變 state 的辦法是觸發 action,action就是一個描述發生什麼的物件。 為了描述 action 如何改變 state 樹,你需要編寫 reducers。

先看一個redux的簡單使用例子:

import { createStore } from 'redux';

// 建立Redux reducer
/**
 * 這是一個 reducer,形式為 (state, action) => state 的純函式。
 * 描述了 action 如何把 state 轉變成下一個 state。
 *
 * state 的形式取決於你,可以是基本型別、陣列、物件,
 * 當 state 變化時需要返回全新的物件,而不是修改傳入的引數。
 *
 * 下面例子使用 `switch` 語句和字串來做判斷,但你可以寫幫助類(helper)
 * 根據不同的約定(如方法對映)來判斷,只要適用你的專案即可。
 */
function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT': 
    return state - 1;
  default:
    return state;
  }
}

// 建立 Redux store 來存放應用的狀態。
// API 是 { subscribe, dispatch, getState }。
let store = createStore(counter);

// 可以手動訂閱更新,也可以事件繫結到檢視層。
store.subscribe(() =>
  console.log(store.getState())
);

// 改變內部 state 惟一方法是 dispatch 一個 action。
// action 可以被序列化,用日記記錄和儲存下來,後期還可以以回放的方式執行
store.dispatch({ type: 'INCREMENT' });
// 1
store.dispatch({ type: 'INCREMENT' });
// 2
store.dispatch({ type: 'DECREMENT' });
// 1
複製程式碼

以上程式碼便是一個redux的最簡單的使用,接下來我們來分別介紹一下redux的三大組成部分:action、reducer以及store。

action

Action 是把資料從應用傳到 store 的有效載荷。它是 store 資料的唯一來源,也就是說要改變store中的state就需要觸發一個action。

Action 本質上一個普通的JavaScript物件。action 內必須使用一個字串型別的 type 欄位來表示將要執行的動作,除了 type 欄位外,action 物件的結構完全由你自己決定。多數情況下,type 會被定義成字串常量。當應用規模越來越大時,建議使用單獨的模組或檔案來存放 action。

import { ADD_TODO, REMOVE_TODO } from '../actionTypes'

//action
{
  type: ADD_TODO,
  text: 'Build my first Redux app'
} 
複製程式碼

提示:使用單獨的模組或檔案來定義 action type 常量並不是必須的,甚至根本不需要定義。對於小應用來說,使用字串做 action type 更方便些。不過,在大型應用中把它們顯式地定義成常量還是利大於弊的。

Action 建立函式

Action 建立函式 就是生成 action 的方法。“action” 和 “action 建立函式” 這兩個概念很容易混在一起,使用時最好注意區分

在 Redux 中的 action 建立函式只是簡單的返回一個 action:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}
複製程式碼

這樣做將使 action 建立函式更容易被移植和測試。

reducer

reducer是根據action 修改state 將其轉變成下一個 state,記住 actions 只是描述了有事情發生了這一事實,並沒有描述應用如何更新 state。

(previousState, action) => newState
複製程式碼

保持 reducer 純淨非常重要。永遠不要在 reducer 裡做這些操作:

  • 修改傳入引數;
  • 執行有副作用的操作,如 API 請求和路由跳轉;
  • 呼叫非純函式,如 Date.now() 或 Math.random()。

提示:reducer 是純函式。它僅僅用於計算下一個 state。它應該是完全可預測的:多次傳入相同的輸入必須產生相同的輸出。它不應做有副作用的操作,如 API 呼叫或路由跳轉。這些應該在 dispatch action 前發生。

//reducer
function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}
複製程式碼

提示:

  • 不要修改 state。 使用 Object.assign() 新建了一個副本。不能這樣使用 Object.assign(state, { visibilityFilter: action.filter }),因為它會改變第一個引數的值。你必須把第一個引數設定為空物件。你也可以開啟對ES7提案物件展開運算子的支援, 從而使用 { ...state,visibilityFilter: action.filter } 達到相同的目的。
  • 在 default 情況下返回舊的 state。遇到未知的 action 時,一定要返回舊的 state。

拆分與合併Reducer

function onAction(state = defaultState, action) {
    switch (action.type) {
        case Types.THEME_CHANGE://主題
            return {
                ...state,
                theme: action.theme,
            };
        case Types.SHOW_THEME_VIEW://主題
            return {
                ...state,
                customThemeViewVisible: action.customThemeViewVisible,
            };
        case Types.SORT_LANGUAGE://排序
            return Object.assign({}, state, {
                checkedArray: action.checkedArray,
            });
        case Types.REFRESH_ABOUT://關於
            return Object.assign({}, state, {
                [action.flag]: {
                    ...state[action.flag],
                    projectModels: action.projectModels,
                }
            });
        case Types.ABOUT_SHOW_MORE://關於
            return Object.assign({}, state, {
                me: {
                    ...state.me,
                    [action.menuFlag]: action.menuShow
                }
            });
        default:
            return state;
    }
}
複製程式碼

上述程式碼看起來有些冗長,並且主題、排序、關於的更新看起來是相互獨立的,能不能將他們拆到單獨的函式或檔案裡呢,答案是可以的。

拆分

//主題 theme.js
export default function onTheme(state = defaultState, action) {
    switch (action.type) {
        case Types.THEME_CHANGE:
            return {
                ...state,
                theme: action.theme,
            };
        case Types.SHOW_THEME_VIEW:
            return {
                ...state,
                customThemeViewVisible: action.customThemeViewVisible,
            };
        default:
            return state;
    }
}

//排序 sort.js
export default function onSort(state = defaultState, action) {
    switch (action.type) {
        case Types.SORT_LANGUAGE:
            return Object.assign({}, state, {
                checkedArray: action.checkedArray,
            });
        default:
            return state;
    }
}

//關於 about.js
export default function onAbout(state = defaultState, action) {
    switch (action.type) {
        case Types.REFRESH_ABOUT:
            return Object.assign({}, state, {
                [action.flag]: {
                    ...state[action.flag],
                    projectModels: action.projectModels,
                }
            });
        case Types.ABOUT_SHOW_MORE:
            return Object.assign({}, state, {
                me: {
                    ...state.me,
                    [action.menuFlag]: action.menuShow
                }
            });
        default:
            return state;
    }
}
複製程式碼

在上述程式碼中,我們將對主題、排序、關於的操作拆到了單獨的函式中並放到了不同的檔案裡,這樣以來各個模組的操作就更加的聚合了,程式碼看起來也就更加的簡潔明瞭。

合併reducer

經過上述的步驟我們將一個大的reducer拆分成了不同的小的reducer,但redux原則是隻允許一個根reducer,接下來我們需要將這幾個小的reducer聚合到一個跟reducer中。

這裡我們需要用到Redux 提供的combineReducers(reducers)

import {combineReducers} from 'redux'
import theme from './theme'
import sort from './sort'
import about from './about'

const index = combineReducers({
    theme: theme,
    sort: sort,
    about: about,
})
export default index;
複製程式碼

combineReducers() 所做的只是生成一個函式,這個函式來呼叫你的一系列 reducer,每個 reducer 根據它們的 key 來篩選出 state 中的一部分資料並處理,然後這個生成的函式再將所有 reducer 的結果合併成一個大的物件。沒有任何魔法。正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都沒有更改 state,那麼也就不會建立一個新的物件。

Store

是儲存state的容器,Store 會把兩個引數(當前的 state 樹和 action)傳入 reducer。

store 有以下職責:

  • 維持應用的 state;
  • 提供 getState() 方法獲取 state;
  • 提供 dispatch(action) 方法更新 state:我們可以在任何地方呼叫 store.dispatch(action),包括元件中、XMLHttpRequest 回撥中、甚至定時器中;
  • 通過 subscribe(listener) 註冊監聽器;
  • 通過 subscribe(listener) 返回的函式登出監聽器。

在前一個章節中,我們使用 combineReducers() 將多個 reducer 合併成為一個。現在我們通過Redux的 createStore()來建立一個Store。

import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
複製程式碼

高階

非同步Action

我們上文中所講的Action都是基於同步實現的,那麼對於網路請求資料庫載入等應用場景同步Action顯然是不適用的,對此我們需要用到非同步Action。

我們可將非同步Action簡答理解為:在Action中進行非同步操作等操作返回後再dispatch一個action。

為了使用非同步action我們需要引入redux-thunk庫,redux-thunk是為Redux提供非同步action支援的中介軟體。

使用redux-thunk

npm install --save redux-thunk

import thunk from 'redux-thunk'
let middlewares = [
    thunk
]
//新增非同步中介軟體redux-thunk
let createAppStore = applyMiddleware(...middlewares)(createStore)
複製程式碼

建立非同步action

export function onSearch(inputKey, token, popularKeys) {
    return dispatch => {
        dispatch({type: Types.SEARCH_REFRESH});
        fetch(genFetchUrl(inputKey)).then(response => {//如果任務取消,則不做任何處理
            return checkCancel(token) ? response.json() : null;
        }).then(responseData => {
            if (!checkCancel(token, true)) {//如果任務取消,則不做任何處理
                return
            }
            if (!responseData || !responseData.items || responseData.items.length === 0) {
                dispatch({type: Types.SEARCH_FAIL, message: inputKey + '什麼都沒找到'});
                return
            }
            let items = responseData.items;
            getFavoriteKeys(inputKey, dispatch, items, token, popularKeys);
        }).catch(e => {
            console.log(e);
            dispatch({type: Types.SEARCH_FAIL, error: e});
        })
    }
}
複製程式碼

非同步資料流

預設情況下,createStore() 所建立的 Redux store 沒有使用 middleware,所以只支援 同步資料流。

你可以使用 applyMiddleware() 來增強 createStore()。它可以幫助你用簡便的方式來描述非同步的 action。

像 redux-thunk 或 redux-promise 這樣支援非同步的 middleware 都包裝了 store 的 dispatch() 方法,以此來讓你 dispatch 一些除了 action 以外的其他內容,例如:函式或者 Promise。你所使用的任何 middleware 都可以以自己的方式解析你 dispatch 的任何內容,並繼續傳遞 actions 給下一個 middleware。比如,支援 Promise 的 middleware 能夠攔截 Promise,然後為每個 Promise 非同步地 dispatch 一對 begin/end actions。

當 middleware 鏈中的最後一個 middleware 開始 dispatch action 時,這個 action 必須是一個普通物件;

總結

  • Redux 應用只有一個單一的 store。當需要拆分資料處理邏輯時,你應該使用 reducer 組合 而不是建立多個 store;
  • redux一個特點是:狀態共享,所有的狀態都放在一個store中,任何component都可以訂閱store中的資料;
  • 並不是所有的state都適合放在store中,這樣會讓store變得非常龐大,如某個狀態只被一個元件使用,不存在狀態共享,可以不放在store中;

未完待續

參考

相關文章