Redux原始碼分析–Reducer篇

lanzhiheng發表於2019-02-26

前兩篇文章分別分析了Redux中介軟體,以及Redux的資料中心的原始碼,如今已經對Redux這個庫有一定程度的瞭解了。目前可以說主菜都已經上齊了,剩下的只能算是飯後甜點了,而今天的甜點是combineReducers這個函式。

Reducer

1. 模組化reducer

《三國演義》裡面有句話是這樣說的

話說天下大勢,分久必合,合久必分

我們寫程式碼的時候也有這種情況**當一個檔案包含的程式碼太多的時候我們會考慮按邏輯把它們拆分成幾個模組,而當我們遇到一些細粒度同類模組的集合時,則會考慮把他們彙總為一個的模組。**至於什麼時候該拆,什麼時候該合,可能不同的領域自有它的權衡方式。

今天主要談談Redux裡面如何模組化管理多個reducer函式。在Redux應用裡reducer函式可以理解成一個處理狀態的函式,它接受一個狀態,以及一個動作,處理之後返回一個更新後的狀態。一個簡單的reducer函式大概如下

function reducerExample(state={}, action) {
  switch (action.type) {
    case `INCREMENT`:
      return Object.assign({}, state, {counter: state.counter + 1})
    case `DECREMENT`:
      return Object.assign({}, state, {counter: state.counter - 1})
    default:
      return state
  }
}
複製程式碼

然而這個函式所包含的邏輯僅僅是對狀態的counter欄位進行加一以及減一操作。Redux是資料中心,它所管理的狀態可能會包含很多個欄位,當欄位相當多的時候,我們需要在reducerExample函式中定義的操作也會漸漸多起來

function reducerExample(state={}, action) {
  switch (action.type) {
    case `INCREMENT`:
      return Object.assign({}, state, {counter: state.counter + 1})
    case `DECREMENT`:
      return Object.assign({}, state, {counter: state.counter - 1})
    case `MULTI`:
      return Object.assign({}, state, {otherCounter: state.otherCounter * 2})
    ....
    // 此處省略100行程式碼
    ....
    default:
      return state
  }
}
複製程式碼

隨著狀態越來越多,操作函式也將會越來越複雜,單一的reducer函式並非長久之計。這也是Redux為何提供combineReducers的原因,它使得我們可以以模組的方式來管理多個reducer函式。

簡單講解該函式的使用,假設Redux管理的應用狀態如下

{
  counter: 0,
  article: {
    title: "",
    content: ""
  }
}
複製程式碼

則我們可以分別定義兩個reducer函式counterReducerarticleReducer

function counterReducer(counter=0, action) {
  ...
}
複製程式碼
function articleReducer(article={}, action) {
  ...
}
複製程式碼

counterReducer裡面只定義與counter欄位有關的資料操作,而在articleReducer裡面只定義與article欄位有關的資料操作,最後通過combineReducers來合併兩個reducer函式,並生成新的函式reducer,我們只需要把這個新的函式reducer與系統進行整合即可。

const reducer = combineReducers({
  counter: counterReducer,
  article: articleReducer
})
複製程式碼

我們甚至可以把counterReducerarticleReducer兩個函式放在不同的檔案中,然後在同一個地方彙總(通過combineReducers)。當我們分發指定的動作之後只有定義了該動作的函式會改變它所對應欄位的狀態資訊。

2. 原始碼分析

接下來分析一下combineReducers函式的工作原理。combineReducers.js這個檔案程式碼加註釋大概100多行,然而我們真正需要了解的核心就僅僅是該指令碼中需要匯出的combineReducers這個函式,其他程式碼大多是用於斷言,暫且略過不談。

1) 收集reducers

我們都知道函式接收物件的每個鍵所對應的值都應該是一個可以用於改變狀態的reducer函式,為此我們會先遍歷combineReducers函式所接收的物件,排除其中不是函式的欄位。

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) { // #1
    const key = reducerKeys[i]

    ....

    if (typeof reducers[key] === `function`) { // #2
      finalReducers[key] = reducers[key]
    }
  }

  ......
}
複製程式碼

程式碼片段#1遍歷函式接收的物件的所有鍵,程式碼片段#2判斷該鍵在原物件中指向的內容是否是一個函式。如果是函式的話,則把該函式以同樣的鍵儲存到finalReducers這個物件中,等迴圈結束以後finalReducers物件的每一個鍵所對應的值則都是一個函式了。

2) 返回一個新的reducer函式

combineReducers其實是一個reducer函式的工廠,在收集不同模組的reducer函式之後,它的責任就是返回一個新的reducer,而這個新的reducer函式能夠排程先前收集的所有reducer。我把後續原始碼中的斷言都去掉之後就剩下下列程式碼

export default function combineReducers(reducers) {
  ...

  const finalReducerKeys = Object.keys(finalReducers) // # 1

  return function combination(state = {}, action) {
    .....

    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key] // #2
      const previousStateForKey = state[key] // #3
      const nextStateForKey = reducer(previousStateForKey, action) // #4
      ....
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}
複製程式碼

首先會在程式碼片段#1獲取先前過濾好的finalReducers物件的所有鍵,並儲存到finalReducerKeys中。然後當前函式會返回一個新的reducer函式,這個函式能夠訪問finalReducers形成一個閉包。

當呼叫這個新的reducer函式的時,它會遍歷finalReducerKeys這個陣列中的每一個鍵,在程式碼#2處獲取當前鍵所對應的reducer函式並儲存到常量reducer,然後在程式碼#3處獲取當前鍵所對應的狀態previousStateForKey

接下來在程式碼#4處以當前狀態previousStateForKey以及action作為引數來呼叫reducer函式,返回該鍵所對應的新狀態nextStateForKey。在每次迭代中都會把當前鍵key作為欄位,把新的狀態儲存到nextState這個物件中去,迴圈結束之後,我們就能夠保證action被充分排程了。

另外,還記得我們門編寫reducer函式的時候會經常使用這種語法嗎?

Object.assign({}, state, {counter: state.counter + 1})
複製程式碼

這表明了我們不會在原來的state基礎上進行修改操作,而是生成了一個新的state,原理大概如下

> a = {}
{}
> b = Object.assign(a, {counter: 1})
{ counter: 1 }
> c = Object.assign({}, a, {counter: 1})
{ counter: 1 }
> a === b
true
> a === c
false
複製程式碼

而在Redux中,正常情況下如果reducer方法被呼叫後並沒有產生新的物件,而只是在原有的物件中進行操作的話,則在繫結元件的時候,狀態的修改將有可能不會引起元件的更新。reducer函式的定位是純函式,不應該造成任何副作用,為此,reducer函式都應該要生成新的物件。

combineReducers這個函式裡也會有相應的處理,這裡需要著重關注hasChanged這個變數

...
  return hasChanged ? nextState : state
...
複製程式碼

當且僅當,這個變數為真值的時候我們才會返回新的狀態,不然的話依舊返回原有的狀態。這個hasChanged是由以下程式碼控制的

...
for (let i = 0; i < finalReducerKeys.length; i++) {
  ....
  hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
複製程式碼

也就是說在所有的迭代中至少有一次迭代符合nextStateForKey !== previousStateForKey這個條件的時候(所對應的reducer返回了新的物件)hasChanged才會為真,新的reducer函式才會返回新的狀態物件nextState。否則將返回原有的狀態物件state,這樣在繫結React元件的時候則有可能會出現狀態資料更新了,元件卻沒有響應的情況。

3. 尾聲

這篇文章簡單地介紹了一下combineReducers這個函式的用法並簡單地分析了combineReducers的原始碼,我們可以通過這個函式來管理多個reducer函式。然而,模組化是把雙刃劍,過度模組化也是不可取的,這得看每個開發者的經驗和權衡能力了。

Happy Coding and Writing !!!

相關文章