重讀redux原始碼(一)

瀟湘待雨發表於2019-02-12

前言

對於react技術棧的前端同學來說,redux應該是相對熟悉的。其程式碼之精簡和設計之巧妙,一直為大家所推崇。此外redux的註釋簡直完美,閱讀起來比較省事。原本也是強行讀了通原始碼,現在也忘得差不多了。因為最近打算對redux進行些操作,所以又開始重讀了redux,收益匪淺。

關於redux的基本概念,這裡就不再詳細描述了。可以參考Redux 中文文件

閱讀原始碼感受

有很多大牛已經提供了很多閱讀經驗。
個人感覺一開始就強行讀原始碼是不可取的,就像我當初讀的第一遍redux,只能說食之無味,現在全忘了。

應該是對其基礎用法比較熟練之後,有問題或者有興趣時再讀比較好,結合文件或者例項,完整的流程走一走。

此外直接原始碼倉庫clone下來,本地跑一跑,實在看不懂的斷點跟進去。

對於不理解的地方,可能是某些方法不太熟悉,這時候多去找找其具體用法和目的

實在不明白的可以結合網上已有的原始碼例項,和別人的思路對比一下,看自己哪裡理解有偏差。

一句話,希望讀過之後對自己有啟發,更深入的理解和學習,而非只是說起來讀過而已。

redux 提供瞭如下方法:

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose
}
複製程式碼

下面的文章就是按照Redux 中文文件例子的順序,來分別看下各方法的實現。

action和actionCreater

定義和概念

action 本質上是 JavaScript 普通物件
在 Redux 中的 actionCreater就是生成 action 的方法

//addTodo 就是actionCreater
function addTodo(text) {
  //return的物件即為action
  return {
    type: ADD_TODO,
    text
  }
}
複製程式碼

在 傳統的 Flux 實現中,當呼叫 action 建立函式時
一般會觸發一個 dispatch
Redux 中只需把 action 建立函式的結果傳給 dispatch() 方法即可發起一次 dispatch 過程。

dispatch(addTodo(text))
//或者 
const boundAddTodo = text => dispatch(addTodo(text))
複製程式碼

當然實際使用的時候,一般情況下(這裡指的是簡單的同步actionCreater)我們不需要每次都手動dispatch,
react-redux 提供的 connect() 會幫我們來做這個事情。

裡面通過bindActionCreators() 可以自動把多個 action 建立函式 繫結到 dispatch() 方法上。

這裡先不涉及connect,我們一起看看bindActionCreators如何實現的。

在看之前,我們可以大膽的猜一下,如果是我們要提供一個warper,將兩個方法繫結在一起會怎麼做:

function a (){
   /*.....*/  
};
function b(f){
  /*.....*/ 
  return f()
}
複製程式碼

b裡面呼叫a(先不考慮其他),通過一個c來繫結一下

function c(){
    return ()=> b(a)
}
複製程式碼

應該就是這麼個樣子,那麼看一下具體實現

bindActionCreators()

先看原始碼:

// 繫結單個actionCreator
function bindActionCreator(actionCreator, dispatch) {
  //將方法dispatch中,避免了action建立手動呼叫。
  return (...args) => dispatch(actionCreator(...args))
}
export default function bindActionCreators(actionCreators, dispatch) {
   // function 說明是單個的actionCreator 直接呼叫bindActionCreator
  if (typeof actionCreators === `function`) {
    return bindActionCreator(actionCreators, dispatch)
  }
   // 校驗,否則拋錯
  if (typeof actionCreators !== `object` || actionCreators === null) {
    throw new Error(`錯誤提示`)
  }
  //獲取keys陣列,以便遍歷
  var keys = Object.keys(actionCreators)
  var boundActionCreators = {}
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i]
    var actionCreator = actionCreators[key]
    //依次進行校驗繫結操作
    if (typeof actionCreator === `function`) {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  //返回
  return boundActionCreators
}
複製程式碼

該方法分為兩部分

首先是 bindActionCreator

對單個ActionCreator方法封裝

   function bindActionCreator(actionCreator, dispatch) {
  //將方法dispatch中,避免了action建立手動呼叫。
  return (...args) => dispatch(actionCreator(...args))
  } 
複製程式碼

bindActionCreators的actionCreators期望是個物件,即actionCreator,
可以想到下面肯定是對該物件進行屬性遍歷,依次呼叫bindActionCreator

下面bindActionCreators的動作就是處理該物件

  • typeof actionCreators === `function`
    單獨的方法,直接呼叫 bindActionCreator結束
  • 如果不是物件或者為null,那麼拋錯
  • 對於物件,根據key進行遍歷,獲取包裝之後的 boundActionCreators 然後返回
  // function 說明是單個的actionCreator 直接呼叫bindActionCreator
  if (typeof actionCreators === `function`) {
    return bindActionCreator(actionCreators, dispatch)
  }
   // 校驗,否則拋錯
  if (typeof actionCreators !== `object` || actionCreators === null) {
    throw new Error(`錯誤提示`)
  }
  //獲取keys陣列,以便遍歷
  var keys = Object.keys(actionCreators)
  var boundActionCreators = {}
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i]
    var actionCreator = actionCreators[key]
    //依次進行校驗繫結操作
    if (typeof actionCreator === `function`) {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }  
複製程式碼

這樣我們獲得了繫結之後的 actionCreators,無需手動呼叫dispatch(同步的簡單情況下)

reducer

action 只是描述了有事發生及提供源資料,具體如何做就需要reducer來處理(詳細介紹就略過了)。
在 Redux 應用中,所有的 state 都被儲存在一個單一物件中

當reducer處理多個atcion時,顯得比較冗長,需要拆分,如下這樣:

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: state.todos.map((todo, index) => {
          if (index === action.index) {
            return Object.assign({}, todo, {
              completed: !todo.completed
            })
          }
          return todo
        })
      })
    default:
      return state
  }
}  
複製程式碼

需要拆分的時候,每個reducer只處理相關部分的state相比於全部state應該更好,
例如:

//reducer1 中 
 function reducer1(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return state.reducer1.a 
      //相比較state只是state.reducer1,顯然好一點
      return state.a
 }
複製程式碼

每個 reducer 只負責管理全域性 state 中它負責的一部分。
每個 reducer 的 state 引數都不同,分別對應它管理的那部分 state 資料

這樣需要在主函式裡,分別對子reducer的入參進行管理,可以如下面這樣:

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}
複製程式碼

當然redux提供了combineReducers()方法

import { combineReducers } from `redux`

const todoApp = combineReducers({
  visibilityFilter,
  todos
})
複製程式碼

那麼我們來看下combineReducers是如何來實現的

combineReducers

還是把完整的程式碼放上來

export default function combineReducers(reducers) {
  // 獲取reducer的key 不作處理的話是子reducer的方法名
  var reducerKeys = Object.keys(reducers)

  var finalReducers = {}
  // 遍歷 構造finalReducers即總的reducer
  for (var i = 0; i < reducerKeys.length; i++) {
    var key = reducerKeys[i]
    if (typeof reducers[key] === `function`) {
      finalReducers[key] = reducers[key]
    }
  }
  var finalReducerKeys = Object.keys(finalReducers)

  var sanityError
  try {
    // 規範校驗
    assertReducerSanity(finalReducers)
  } catch (e) {
    sanityError = e
  }

  return function combination(state = {}, action) {
    if (sanityError) {
      throw sanityError
    }
    // 警報資訊 
    if (process.env.NODE_ENV !== `production`) {
      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action)
      if (warningMessage) {
        warning(warningMessage)
      }
    }
    /**
     * 當有action改變時,
     * 遍歷finalReducers,執行reducer並賦值給nextState,
     * 通過對應key的state是否改變決定返回當前或者nextState
     * */
    // state改變與否的flag 
    var hasChanged = false
    var nextState = {}
    // 依次處理
    for (var i = 0; i < finalReducerKeys.length; i++) {
      var key = finalReducerKeys[i]
      var reducer = finalReducers[key]
      // 獲取對應key的state屬性
      var previousStateForKey = state[key]
      // 目的之一,只處理對應key資料
      var nextStateForKey = reducer(previousStateForKey, action)
      // 不能返回undefined,否則拋錯
      if (typeof nextStateForKey === `undefined`) {
        var errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      // 新狀態賦給 nextState物件
      nextState[key] = nextStateForKey
      // 是否改變處理 
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 視情況返回state
    return hasChanged ? nextState : state
  }
}
複製程式碼

入參

首先看一下入參:reducers

  • 即需要合併處理的子reducer物件集合。
  • 可以通過import * as reducers來獲取
  • tips:
    reducer應該對default情況也進行處理, 當state是undefined或者未定義的action時,也不能返回undefined。
    返回的是一個總reducer,可以呼叫每個傳入方法,並且分別傳入相應的state屬性。

遍歷reducers

既然是個物件集合,肯定要遍歷物件,所以前幾步就是這麼個操作。

// 獲取reducer key  目的在於每個子方法處理對應key的state
  var reducerKeys = Object.keys(reducers)

  var finalReducers = {}
  // 遍歷 構造finalReducers即總的reducer
  for (var i = 0; i < reducerKeys.length; i++) {
    var key = reducerKeys[i]
    if (typeof reducers[key] === `function`) {
      finalReducers[key] = reducers[key]
    }
  }
//獲取finalReducers 供下面遍歷呼叫  
var finalReducerKeys = Object.keys(finalReducers)
複製程式碼

然後是規範校驗,作為一個框架這是必須的,可以略過

combination

返回一個function

  • 當action被dispatch進來時,該方法主要是分發不同state到對應reducer處理,並返回最新state

  • 先是標識變數:

    // state改變與否的flag 
    var hasChanged = false
    var nextState = {}  
複製程式碼
  • 進行遍歷finalReducers
    儲存原來的previousStateForKey

  • 然後分發對應屬性給相應reducer進行處理獲取nextStateForKey

    先對nextStateForKey 做個校驗,因為reducer要求做相容的,所以不允許undefined的出現,出現就拋錯。

    正常的話就nextStateForKey把賦給nextState對應的key

  • 前後兩個state做個比較看是否相等,相等的話hasChanged置為true
    遍歷結束之後就獲得了一個新的state即nextState

for (var i = 0; i < finalReducerKeys.length; i++) {
      var key = finalReducerKeys[i]
      var reducer = finalReducers[key]
      // 獲取對應key的state屬性
      var previousStateForKey = state[key]
      // 目的之一,只處理對應key資料
      var nextStateForKey = reducer(previousStateForKey, action)
      // 不能返回undefined,否則拋錯
      if (typeof nextStateForKey === `undefined`) {
         //.....
      }
      // 新狀態賦給 nextState物件
      nextState[key] = nextStateForKey
      // 是否改變處理 
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
複製程式碼

根據hasChanged來決定返回新舊state。

// 視情況返回state
    return hasChanged ? nextState : state
複製程式碼

到這裡combineReducers就結束了。

結束語

這次先分享一半,還是有點多的,剩下的下次再記錄一下。拋磚引玉,提升自己,共同學習吧。

參考文章

Redux 中文文件

相關文章