1. 程式人生 > >React高階元件原理與在Redux中的實踐

React高階元件原理與在Redux中的實踐

建議在github閱讀,我會及時更新部分內容。也歡迎star,issue

1.高階reducer的定義

高階reducer指的是一個函式,該函式接收一個reducer函式作為引數或者返回一個reducer函式作為函式的返回值。高階reducer也可以被看做為一個reducer工廠,combineReducers是高階reducer一個典型的例子。我們可以使用高階reducer函式來建立一個符合自己要求的reducer的函式。

2.為什麼要高階reducer函式

當應用功能變大的時候,在reducer函式中那些通用的邏輯就會出現重複。你會發現很多reducer都是處理同樣的邏輯,只是處理的資料不同而已,所以我們就會想著如何重複使用reducer函式中那些通用的邏輯,從而減小應用的程式碼。同時,有時候你想要在store中處理特定型別資料的多個”例項”(如下面的counter函式需要用於多個地方)。然而,redux的全域性store採用了一些權衡:雖然,我們可以很容易跟蹤整個應用的state狀態(for迴圈,所有的reducer都執行了一遍),但是,很難針對特定的action來更新特定的state的某一部分資料,特別是當你使用combineReducers(因為dispatch的時候,我們採用的是for迴圈來對

每一個reducer都進行了執行)。

例如下面的例子,我們想要跟蹤應用中多個counter例項,分別為counterA,counterB,counterC。我們首先定義了counter這個reducer,同時使用了combineReducers來管理狀態:

function counter(state = 0, action) {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1
; default: return state; } } const rootReducer = combineReducers({ counterA : counter, counterB : counter, counterC : counter });

針對這種情況存在一個問題,因為我們的combineReducer採用的是for迴圈來對所有的reducer使用相同的action都執行一遍(不知道的請點選這裡,檢視combineReducer部分分析)。所以如果我們dispacth({type:”INCREMENT”}),那麼,我們上面counterA,counterB,counterC都會執行一遍,而不是執行某一個reducer函式。所以我們需要使用一種機制來保證只有一個reducer會執行,而這種機制就是我們要說的高階reducer。

3.高階reducer的用法

指定一個reducer的最常用的方式就是使用一個字尾或者字首來產生一個reducer的action,或者將額外的資訊新增到action物件上。下面是幾個例子:

3.1 常規高階reudcer函式

//這是一個高階reducer,因為它會返回一個reducer函式作為返回值
function createCounterWithNamedType(counterName = '') {
    return function counter(state = 0, action) {
        switch (action.type) {
            case `INCREMENT_${counterName}`:
                return state + 1;
            case `DECREMENT_${counterName}`:
                return state - 1;
            default:
                return state;
        }
    }
}
//這裡和上面的高階reducer是一樣的,只是方式不同而已
function createCounterWithNameData(counterName = '') {
    return function counter(state = 0, action) {
        const {name} = action;
        if(name !== counterName) return state;
        //如果是我們關注的counterName名稱,那麼我們才會通過action的type進行處理
        //否則原樣返回state
        switch (action.type) {
            case `INCREMENT`:
                return state + 1;
            case `DECREMENT`:
                return state - 1;
            default:
                return state;
        }
    }
}

下面我們可以使用上面任意一個函式來產生我們自己的reducer,然後dispatch一個action,而該action只會影響我們關心的那部分的state的值:

const rootReducer = combineReducers({
    counterA : createCounterWithNamedType('A'),
    counterB : createCounterWithNamedType('B'),
    counterC : createCounterWithNamedType('C'),
});
//redux的dispatch方法會遍歷所有的reducer來計算下一個狀態
//subscribe用於計算完成後進行回撥
store.dispatch({type : 'INCREMENT_B'});
//當你呼叫store的dispatch的時候我們會遍歷上面指定的所有的reducers,然後發現只有
//第二個reducer,即createCounterWithNamedType('B')能夠處理,其他reducer原樣返回
//state的當前狀態。注意:每一個應用只有一個store,combineReducers每一個key負責管理
//store的一部分狀態
console.log(store.getState());
// {counterA : 0, counterB : 1, counterC : 0}
// store中只有counterB發生變化

3.2 通用高階reudcer函式

我們也可以通過下面的方式來產生一個更加通用的高階reducer,該reducer同時接收一個指定的reducer函式以及一個name或者identifier:

function counter(state = 0, action) {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state;
    }
}
//通用reducer工廠函式,接收一個reducer函式和一個name作為引數,返回一個通用reducer
function createNamedWrapperReducer(reducerFunction, reducerName) {
    return (state, action) => {
        const {name} = action;
        //dispatch的這個action必須有一個name屬性,用於判斷你要執行哪一個reducer
        const isInitializationCall = state === undefined;
        if(name !== reducerName && !isInitializationCall) return state;
        //如果傳遞的action.name不是該reducerName指定的reducer處理,那麼返回當前state
        //否則通過我們的reducer函式來處理。和上面這個例子一樣
        return reducerFunction(state, action);    
    }
}
const rootReducer = combineReducers({
    counterA : createNamedWrapperReducer(counter, 'A'),
    counterB : createNamedWrapperReducer(counter, 'B'),
    counterC : createNamedWrapperReducer(counter, 'C'),
});

其實上面的這種邏輯已經有一個庫實現了,即
multireducer

3.3 含有通用過濾函式的高階reudcer函式

//和上面不一樣的是,這個reducer工廠函式接收的第二個引數是一個函式,而不是一個reducerName,這個函式用於對我們dispatch這個action進行過來
function createFilteredReducer(reducerFunction, reducerPredicate) {
    return (state, action) => {
        const isInitializationCall = state === undefined;
        const shouldRunWrappedReducer = reducerPredicate(action) || isInitializationCall;
        //傳入我們的action到reducer的filter函式中,如果返回true,那麼我們會執行reducer函式,如果返回false,我們返回當前state狀態即可
        return shouldRunWrappedReducer ? reducerFunction(state, action) : state;
    }
}
const rootReducer = combineReducers({
    // check for suffixed strings
    counterA : createFilteredReducer(counter, action => action.type.endsWith('_A')),
    // check for extra data in the action
    counterB : createFilteredReducer(counter, action => action.name === 'B'),
    // respond to all 'INCREMENT' actions, but never 'DECREMENT'
    counterC : createFilteredReducer(counter, action => action.type === 'INCREMENT')
});

上面的combineReducers整合的這個rootReducer可以處理的action如下:

store.dispatch({type:"XXX_A"})
//如果你dispatch的這個action.type字尾為_A,那麼會通過counter進行處理(注意:這個counter肯定和我們上面的counter有點差異,它會多出很多switch的case,因為這裡對我們的action的type有依賴)
//但是這裡是type的判斷情況,如果是下面的name就不會對switch的case產生影響,因為對type沒有影響
store.dispatch({name:"B"})
//此時我們還可以新增type屬性來讓我們上面的counter產生作用,是DECREMENT/INCREMENT
store.dispatch({type:"INCREMENT"})

4.高階reducer的幾個通用庫

multireducer我上面已經說過了,你可以直接去官網檢視。很顯然,它實現了一種機制,解決我們了第二部分表述的那種dispatch一個action後,所有的reducer都執行了一遍的情況。具體使用檢視API。但是這個庫不是我想說的,我想說的是violet-paginator,我是因為看了這個庫的用法才深入瞭解了高階reducer的內容。

import { createPaginator } from 'violet-paginator';
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
 const reducer = combineReducers({
    recipes: createPaginator(config)
  //很顯然createPaginator返回值必須是一個reducer函式,這個函式用於對分頁處理
  })

下面是具體的config內容:

export default {
  listId: 'recipes',
  fetch: mockFetch,
  //會使用這個fetch方法傳送請求,毋須使用者手動干預
  pageParams: {
    totalCountProp: 'totalCount'
   //伺服器通過那個欄位來返回我們總的欄位個數
   //https://sslotsky.gitbooks.io/violet-paginator/content/v/v2.0.0/single_list_configuration.html
  },
  initialSettings: {
    pageSize: 1
  }
}

config中的內容就是為我們的reducer工廠函式(高階函式)傳入了配置資訊,所有的操作都會在該工廠函式中完成,其中包括從伺服器端獲取資料(通過fetch函式來完成獲取資料)

其中元件中的用法是:

   <VioletPaginator listId="recipes" />
   <VioletDataTable listId="recipes" />

其中我比較糾結的不是如何用的,而是上面配置的listId=”recipes”的作用是什麼,所以我好奇的看了下內部的實現:

//返回一個字串
 function actionType(t, id) {
  return `${t}_${id}`
}
export const INITIALIZE_PAGINATOR = '@@violet-paginator/INITIALIZE_PAGINATOR'
//這裡只給出了一個actionTypes的值
export default function createPaginator(config) {
  const { initialSettings } = registerPaginator(config)
  const resolve = t => actionType(t, config.listId)
  ////呼叫resolve得到一個字串
  return resolveEach(defaultPaginator.merge(initialSettings), {
    [actionTypes.EXPIRE_ALL]: expire,
    [resolve(actionTypes.INITIALIZE_PAGINATOR)]: initialize,
    //resolve呼叫得到"@@violet-paginator/INITIALIZE_PAGINATOR_recipes"
    [resolve(actionTypes.EXPIRE_PAGINATOR)]: expire
  })
}

所以,從這裡你大概可以看出來,我們點選了VioletPaginator的時候,listId=”recipes”決定過了我們發出的這個action的資訊,而且應該是type資訊。我們在dispatch的時候,會將所有的combineReducers中的key對應的函式都執行一遍!

5.高階reducer的好處

通過上面的例子你也可以看到,對於counterA,counterB,counterC我們只用提供一個reducer函式即counter,但是我們卻可以複用這部分的邏輯。只要我們dispatch的這個action明確指定我們需要改變哪一部分state狀態即可,如:

store.dispatch({name:"A",type:"INCREMENT"})
//此時只會改變我們counterA對應的那部分的state的狀態,同時將狀態的值加1

參考資料: