Redux原始碼分析--bindActionCreators篇

lanzhiheng發表於2018-07-15

這是Redux原始碼分析系列的第四篇文章,當這篇文章結束之後Redux原始碼分析系列也該告一段落了。這篇文章主要想談談bindActionCreators這個函式的實現原理,為了更好的理解這個函式我會恰當地引入一些應用程式碼。

Action Creator

1. ActionCreator建立動作

在深入分析原始碼之前我們先來聊聊ActionCreator。從字面上理解,它是一個動作的創造者,或者說是動作的工廠。如果我們想根據不同的引數來生成不同步長的計數器動作,則可以把工廠函式宣告為

const counterIncActionCreator = function(step) {
  return {
    type: 'INCREMENT',
    step: step || 1
  }
}
複製程式碼

隨著業務邏輯越來越複雜,我們可以通過定義更加複雜的工廠函式來生成更多樣化的動作型別。

2. bindActionCreator高階函式

從上述的例子出發,如果我們想生產出不同步長的計數器動作,並分發他們,則需要把程式碼寫成下面這樣子

// 為了簡化程式碼我把dispatch函式定義為只有列印功能的函式
const dispatch = function(action) {
  console.log(action)
}

const action1 = counterIncActionCreator()
dispatch(action1) // { type: 'INCREMENT', step: 1 }

const action2 = counterIncActionCreator(2)
dispatch(action2) // { type: 'INCREMENT', step: 2 }

const action3 = counterIncActionCreator(3)
dispatch(action3) // { type: 'INCREMENT', step: 3 }
複製程式碼

可見每次分發動作之前我們都得手動呼叫counterIncActionCreator來生產相應的動作,這種方式並不是那麼的優雅。這個時候我們就可以採用bindActionCreators這個檔案裡面的bindActionCreator工具函式來優化程式碼了,該函式的原始碼如下

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}
複製程式碼

bindActionCreator將會返回一個新函式,這個函式會用自身所接收的引數來呼叫actionCreator並生成對應動作,並且這個生成的動作將會作為dispatch函式的引數。也就是說我們把

  1. 生成動作
  2. 排程動作

這兩個步驟都封裝到一個函式裡面了,於是便得到了更為優雅的排程過程

...
const increment = bindActionCreator(counterIncActionCreator, dispatch)

increment() // { type: 'INCREMENT', step: 1 }

increment(2) // { type: 'INCREMENT', step: 2 }

increment(3) // { type: 'INCREMENT', step: 3 }
複製程式碼

3. bindActionCreators

接下來看看bindActionCreators這個函式,它是bindActionCreator函式的加強版。刪掉一些斷言語句之後原始碼如下

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') { // #1
    return bindActionCreator(actionCreators, dispatch) // #2
  }

  ....

  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') { // #3
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}
複製程式碼

程式碼#1的判斷語句是為了做相容處理,當接收的引數actionCreators為一個函式的時候,則認為它是單一的動作工廠,便在程式碼#2處直接呼叫bindActionCreator工具函式來封裝排程過程。

另一情況是當actionCreators引數是一個物件的時候,則遍歷這個物件。程式碼#3會判斷每個鍵在原始物件中的值是否是個函式,如果是一個函式則認為它是一個動作工廠,並使用bindActionCreator函式來封裝排程過程,最後把生成的新函式以同樣的鍵key儲存到boundActionCreators物件中。在函式的末尾會返回boundActionCreators物件。

舉個簡單應用例子,首先使用一個物件來儲存兩個事件工廠

const MyActionCreators = {
  increment: function(step) {
    return {
      type: 'INCREMENT',
      step: step || 1
    }
  },

  decrement: function(step) {
    return {
      type: 'DECREMENT',
      step: - (step || 1)
    }
  }
}
複製程式碼

然後通過bindActionCreators來封裝排程過程,並返回一個新的物件

const dispatch = function(action) {
  console.log(action)
}

const MyNewActionCreators = bindActionCreators(MyActionCreators, dispatch)
複製程式碼

最後我們便可以用新的物件來主導排程過程了

MyNewActionCreators.increment() // { type: 'INCREMENT', step: 1 }
MyNewActionCreators.increment(2) // { type: 'INCREMENT', step: 2 }
MyNewActionCreators.increment(3) // { type: 'INCREMENT', step: 3 }
MyNewActionCreators.decrement() // { type: 'DECREMENT', step: -1 }
MyNewActionCreators.decrement(2) // { type: 'DECREMENT', step: -2 }
MyNewActionCreators.decrement(3) // { type: 'DECREMENT', step: -3 }
複製程式碼

這種排程方式顯然比原始的依次呼叫的方式更為優雅

// 原始的排程方式
dispatch(MyActionCreators.increment()) // { type: 'INCREMENT', step: 1 }
dispatch(MyActionCreators.increment(2)) // { type: 'INCREMENT', step: 2 }
dispatch(MyActionCreators.increment(3)) // { type: 'INCREMENT', step: 3 }
dispatch(MyActionCreators.decrement()) // { type: 'DECREMENT', step: -1 }
dispatch(MyActionCreators.decrement(2)) // { type: 'DECREMENT', step: -2 }
dispatch(MyActionCreators.decrement(3)) // { type: 'DECREMENT', step: -3 }
複製程式碼

4. 尾聲

這篇文章分析了高階函式bindActionCreators的原始碼。它並不是什麼複雜的函式,然而通過這類高階函式我們就可以把原來笨重的函式呼叫過程封裝起來,使最終的業務程式碼更加優雅。

Happy Coding and Writing!!!

相關文章