Redux中的編程藝術

Redux源碼分析已經滿大街都是了。可是大多都是介紹如何實現,實現原理。而忽略了Redux代碼中隱藏的知識點和藝術。爲何稱之爲藝術,是這些簡短的代碼蘊含着太多前端同窗應該掌握的JS知識以及巧妙的設計模式的運用。前端

createStore 不只僅是一個API

...
export default function createStore(reducer, preloadedState, enhancer) {
  ...
  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  function ensureCanMutateNextListeners() {
    ...
  }

  function getState() {
    ...
    return currentState
  }

  function subscribe(listener) {
    ...
  }

  function dispatch(action) {
    ...
    return action
  }

  function replaceReducer(nextReducer) {
    ...
  }

  function observable() {
    ...
  }

  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}
複製代碼

這段代碼,蘊含着不少知識。react

首先是經過閉包對內部變量進行了私有化,外部是沒法訪問閉包內的變量。其次是對外暴露了接口來提供外部對內部屬性的訪問。這實際上是典型的「沙盒模式」。面試

沙盒模式幫咱們保護內部數據的安全性,在沙盒模式下,咱們只能經過return出來的開放接口才能對沙盒內部的數據進行訪問和操做。redux

雖然屬性被保護在沙盒中,可是因爲JS語言的特性,咱們沒法徹底避免用戶經過引用去修改屬性。設計模式

subscribe/dispatch 訂閱發佈模式

subscribe 訂閱

Redux經過subscribe接口註冊訂閱函數,並將這些用戶提供的訂閱函數添加到閉包中的nextListeners中。數組

最巧妙的是考慮到了會有一部分開發者會有取消訂閱函數的需求,並提供了取消訂閱的接口。安全

這個接口的'藝術'並不只僅是實現一個訂閱模式,還有做者嚴謹的代碼風格。前端工程師

if (typeof listener !== 'function') {
  throw new Error('Expected the listener to be a function.')
}
複製代碼

充分考慮到入參的正確性,以及經過isDispatchingisSubscribed來避免意外發生。閉包

其實這個實現也是一個很簡單的高階函數的實現。是否是常常在前端面試題裏面看到?(T_T)app

這讓我想起來了。不少初級,中級前端工程師調用完addEventListener就忘記使用removeEventListener最終致使不少閉包錯誤。因此,記得在不在使用的時候取消訂閱是很是重要的。

dispatch 發佈

經過Reduxdispatch接口,咱們能夠發佈一個action對象,去通知狀態須要作一些改變。

一樣在函數的入口就作了嚴格的限制:

if (!isPlainObject(action)) {
  throw new Error(
    'Actions must be plain objects. ' +
      'Use custom middleware for async actions.'
  )
}

if (typeof action.type === 'undefined') {
  throw new Error(
    'Actions may not have an undefined "type" property. ' +
      'Have you misspelled a constant?'
  )
}

if (isDispatching) {
  throw new Error('Reducers may not dispatch actions.')
}
複製代碼

不得不說,做者在代碼健壯性的考慮是很是周全的,真的是自嘆不如,我如今基本上是隻要本身點不出來問題就直接提測。 (T_T)

下面的代碼更嚴謹,爲了保障代碼的健壯性,以及整個ReduxStore對象的完整性。直接使用了try { ... } finally { ... }來保障isDispatching這個內部全局狀態的一致性。

再一次跪服+掩面痛哭 (T_T)

後面就是執行以前添加的訂閱函數。固然訂閱函數是沒有任何參數的,也就意味着,使用者必須經過store.getState()來取得最新的狀態。

observable 觀察者

從函數字面意思,很容易猜到observable是一個觀察者模式的實現接口。

function observable() {
  const outerSubscribe = subscribe
  return {
    subscribe(observer) {
      if (typeof observer !== 'object' || observer === null) {
        throw new TypeError('Expected the observer to be an object.')
      }

      function observeState() {
        if (observer.next) {
          observer.next(getState())
        }
      }

      observeState()
      const unsubscribe = outerSubscribe(observeState)
      return { unsubscribe }
    },

    [$$observable]() {
      return this
    }
  }
}
複製代碼

在開頭,就將訂閱接口進行了攔截,而後返回一個新的對象。這個對象爲用戶提供了添加觀察對象的接口,而這個觀察對象須要具備一個next函數。

combineReducers 又雙叒叕見「高階函數」

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

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }

    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}
複製代碼

再一次被做者的嚴謹所折服,從函數開始就對參數的有效性進行了檢查,而且只有在非生產模式才進行這種檢查。並在assertReducerShape中對每個註冊的reducer進行了正確性的檢查用來保證每個reducer函數都返回非undefined值。

哦!老天,在返回的函數中,又進行了嚴格的檢查(T_T)。而後將每個reducer的返回值從新組裝到新的nextState中。並經過一個淺比較來決定是返回新的狀態仍是老的狀態。

bindActionCreators 仍是高階函數

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

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

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${ actionCreators === null ? 'null' : typeof actionCreators }. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }

  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') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}
複製代碼

我平時是不多用這個API的,可是這並不阻礙我去欣賞這段代碼。可能這裏是我惟一可以吐槽大神的地方了for (let i = 0; i < keys.length; i++) {,固然他在這裏這麼用其實並不會引發什麼隱患,可是每次循環都要取一次length也是須要進行一次多餘計算的(^_^)v,固然上面代碼也有這個問題。

其實在開始位置的return dispatch(actionCreator.apply(this, arguments))apply(this)的使用更是很是的666到飛起。

通常咱們會在組件中這麼作:

import { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'

import * as TodoActionCreators from './TodoActionCreators'
console.log(TodoActionCreators)

class TodoListContainer extends Component {
  componentDidMount() {
    let { dispatch } = this.props
    let action = TodoActionCreators.addTodo('Use Redux')
    dispatch(action)
  }

  render() {
    let { todos, dispatch } = this.props

    let boundActionCreators = bindActionCreators(TodoActionCreators, dispatch)
    console.log(boundActionCreators)

    return <TodoList todos={todos} {...boundActionCreators} /> } } export default connect( state => ({ todos: state.todos }) )(TodoListContainer) 複製代碼

當咱們使用bindActionCreators建立action發佈函數的時候,它會自動將函數的上下文(this)綁定到當前的做用域上。可是一般我爲了解藕,並不會在action的發佈函數中訪問this,裏面只存放業務邏輯。

再一個還算能夠吐槽的地方就是對於Object的判斷,對於function的判斷重複出現屢次。固然,單獨拿出來一個函數來進行調用,性能代價要比直接寫在這裏要大得多。

applyMiddleware 強大的聚合器

import compose from './compose'

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
複製代碼

經過前面的代碼,咱們能夠發現applayMiddleware其實就是包裝enhancer的工具函數,而在createStore的開始,就對參數進行了適配。

一般咱們會像下面這樣註冊middleware

const store = createStore(
  reducer,
  preloadedState,
  applyMiddleware(...middleware)
)
複製代碼

或者

const store = createStore(
  reducer,
  applyMiddleware(...middleware)
)
複製代碼

因此,咱們會驚奇的發現。哦,原來咱們把applyMiddleware調用放到第二個參數和第三個參數都是同樣的。因此咱們也能夠認爲createStore也實現了適配器模式。固然,貌似有一些牽強(T_T)。

關於applyMiddleware,也許最複雜的就是對compose的使用了。

const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
複製代碼

經過以上代碼,咱們將全部傳入的middleware進行了一次剝皮,把第一層高階函數返回的函數拿出來。這樣chain實際上是一個(next) => (action) => { ... }函數的數組,也就是中間件剝開後返回的函數組成的數組。 而後經過compose對中間件數組內剝出來的高階函數進行組合造成一個調用鏈。調用一次,中間件內的全部函數都將被執行。

function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製代碼

所以通過compose處理後,傳入中間件的next實際上就是store.dispatch。而這樣處理後返回的新的dispatch,就是通過applyMiddleware第二次剝開後的高階函數(action) => {...}組成的函數鏈。而這個函數鏈傳遞給applyMiddleware返回值的dispatch屬性。

而經過applyMiddleware返回後的dispatch被返回給store對象內,也就成了咱們在外面使用的dispatch。這樣也就實現了調用dispatch就實現了調用全部註冊的中間件。

結束語

Redux的代碼雖然只有短短几百行,可是蘊含着不少設計模式的思想和高級JS語法在裏面。每次讀完,都會學到新的知識。而做者對於高階函數的使用是你們極好的參考。

固然本人涉足JS開發時間有限。會存在不少理解不對的地方,但願大咖指正。

相關文章
相關標籤/搜索