Redux中的程式設計藝術

YaHuiLiang(Ryou)發表於2018-06-18

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
  }
}
複製程式碼

這段程式碼,蘊含著很多知識。

首先是通過閉包對內部變數進行了私有化,外部是無法訪問閉包內的變數。其次是對外暴露了介面來提供外部對內部屬性的訪問。這其實是典型的“沙盒模式”。

沙盒模式幫我們保護內部資料的安全性,在沙盒模式下,我們只能通過return出來的開放介面才能對沙盒內部的資料進行訪問和操作。

雖然屬性被保護在沙盒中,但是由於JS語言的特性,我們無法完全避免使用者通過引用去修改屬性。

subscribe/dispatch 訂閱釋出模式

subscribe 訂閱

Redux通過subscribe介面註冊訂閱函式,並將這些使用者提供的訂閱函式新增到閉包中的nextListeners中。

最巧妙的是考慮到了會有一部分開發者會有取消訂閱函式的需求,並提供了取消訂閱的介面。

這個介面的'藝術'並不僅僅是實現一個訂閱模式,還有作者嚴謹的程式碼風格。

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

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

其實這個實現也是一個很簡單的高階函式的實現。是不是經常在前端面試題裡面看到?(T_T)

這讓我想起來了。很多初級,中級前端工程師呼叫完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開發時間有限。會存在很多理解不對的地方,希望大咖指正。

相關文章