1. 程式人生 > >對redux的認識(原始碼深度解讀)

對redux的認識(原始碼深度解讀)

用redux寫過一些小專案,感覺有段時間不用快要忘記。所以寫下我對redux的理解(和react-redux結合使用)。作為筆記。redux版本是3.6.0。react-redux版本是5.0.4

首先開啟github的redux專案,在原始碼中,主要有以下檔案:
這裡寫圖片描述

這也是redux的組成部分。utils資料夾只有一個warning.js,用來列印錯誤資訊。index.js用來匯出相關內容。關鍵的是其他五個檔案。

applyMiddleware.jsbindActionCreator.jscombineReducers.jscompose.jscreateStore.js

一個redux的流程是這樣的。

這裡寫圖片描述

1、在View上,使用者會做出一個動作(比如點選)。

2、Store通過其方法dispatch這個動作,這個動作往往是一個物件({type: 'click', payload: {}}),或者是一個函式(需要redux-thunk或其他中介軟體處理)。對於Action,一般都是大寫並且宣告為常量。

store.dispatch({
  type: 'SELECT_BOOKNAME',
  payload: {
    text: 'react'
  }
})

3、Reducers會收到這個動作,並進行相應的處理。

import { SELECT_BOOKNAME } from '../actions'
; const selectedBookName = (state = 'java', action) => { switch (action.type) { case SELECT_BOOKNAME: return action.payload.text; default: return state; } } export default selectedBookName;

reduce處理會改變應用的state(redux應用中,只有一個store)。如上action.payload.text。預設情況下state的值為java,現在變成了react

(action中傳過來的引數)。但是這個state不是應用整個的state,正如這個reducer只是一個子reducer而已。所以這裡作為引數的state實際上是應用的state.selectedBookName屬性。

4、state改變了,一般而言,頁面(View)會重新render。依賴於state的資料也會改變。

下面是原始碼分析

compose

本來把compose放在後面,想想還是提前了。
compose是一個輔助函式,效果很簡單,它只是簡化了深層巢狀呼叫函式的問題。compose(f, g)的行為等價於(...args) => f(g(...args))。簡單的說,compose(f, g)是一個函式,接受的引數會傳給函式g,函式g執行,將函式返回的結果作為函式f的引數。然後執行函式fcompose(f, g)的返回值即函式f執行後的返回值。

export default 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)))
}

這裡主要使用的是Array.prototype.reduce函式reduce(function(acc, currentValue, currentIndex, arr), initialValue)。其中initialValue是可選的。如果提供了initialValue,那麼acc的值是initialValuecurrentValue的值是陣列的第一個元素。如果沒提供initialValue,那麼acc的值是陣列的第一個值,currentValue的值是陣列的第二個值。

Array.prototype.reduce函式中的引數函式(callback)將會從左到右處理陣列的每一個值。然後將最終的結果返回。在這裡,callback返回一個函式。

(...args) => a(b(...args));

所以compose函式的返回值也是一個函式(A),其接受一個或多個引數,然後將引數傳入到compose函式中最後一個引數執行。執行的結果作為compose函式中倒數第二個函式的引數。依次執行。

compose(fn1, fn2, fn3)(1,2,3);
// 等價於
fn1(fn2(fn3(1,2,3)))

上述程式碼將引數1,2,3作為fn3的引數。fn3(1,2,3)執行的結果作為fn2的引數…

createStore

去掉了註釋和輔助函式以及store的一些函式的具體的實現(dispatch、getState、subscribe、replaceReducer)。將精力全部集中到createStore這個函式。

// createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState) // 注1
  }
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false
  function ensureCanMutateNextListeners() {}  
  function subscribe(listener) {}
  function dispatch(action) {}
  function replaceReducer(nextReducer) {}
  function observable() {}

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

在建立store的時候,我們可以傳入初始狀態(preloadedState),但這不是必須的。所以一開始會判斷。

enhancer一般是applyMiddleware或者是compose函式。通過它可以使用一些第三方元件,比如redux-loggerredux-thunk等。這個引數也是可選的。

一般我們建立的store是這樣的。

const store = createStore(
   reducers,
   preloadState,
   compose(
     applyMiddleware(...middleware),
     window.devToolsExtension ? window.devToolsExtension() : f => f
   )
 )
 // 或者是
 const store = createStore(
   reducers,
   preloadState,
   applyMiddleware(...middleware)
 )

不管如何,如果有enhancer,那麼就會轉而執行enhancer(注1)。假設這裡是applyMiddleware。執行applyMiddleware並傳入引數。並把函式本身傳入作為引數。然後執行enhancer(createStore)(返回的肯定是一個函式,從下文applyMiddleware可以看到)並傳入(reducer, preloadedState)

return enhancer(createStore)(reducer, preloadedState) // 注1

applyMiddleware

// applyMiddleware.js
export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => { // 注2
    const store = createStore(reducer, preloadedState, enhancer) // 注3
    let dispatch = store.dispatch
    let chain = []

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

    return {
      ...store,
      dispatch
    }
  }
}

注2這裡不是很容易理解的,需要對照上文的createStore

return enhancer(createStore)(reducer, preloadedState) //1
return (createStore) => (reducer, preloadedState, enhancer) => { //2

相互對照,方便理解。那麼流程就是在createStore的時候,如果有enhancer(如applyMiddleware),就先執行enhancer,並傳入createStore函式本身以及它的引數reducer, preloadState

再檢視注3,也就是applyMiddleware的開始部分。因為applyMiddleware的引數是一個個元件,對於這些元件,執行createStore函式,並傳入第一次createStore傳下來的兩個引數。所以流程就是createStore->applyMiddleware->createStore。由於這次的enhancer是undefined,所以在createStore.js中會繼續往下執行,返回一個store。繞了個彎。

const store = createStore(reducer, preloadedState, enhancer)

然後從注3往下執行。關鍵來了。先插述點東西。

————————————————————————-
one:
以中介軟體的最簡單方式為例,建立一箇中間件通常是這樣的(如logger中介軟體):

const logger = store => next => action => { // next相當於dispatch
  console.log('dispatching', action);
  let result = next(action); // 相當於dispatch(action),返回的結果仍然是action
  console.log('next state', store.getState()); // 注4
  return result; // 返回action,供其他中介軟體處理
}

————————————————————————-
插述完畢。
之前說到從注3往下執行。接下來分析這一塊程式碼:

// applyMiddleware.js
let dispatch = store.dispatch
let chain = []

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

middlewares是所有中介軟體組成的陣列。呼叫map方法,傳入中介軟體(是一個函式,比如上述的logger中介軟體),並執行這個函式,傳入middlewareAPI變數(相當於store)。

const logger = store => next => action => { // next相當於dispatch,往下文看
  console.log('dispatching', action);
  let result = next(action); // 相當於dispatch(action),返回的結果仍然是action
  console.log('next state', store.getState()); // 注4
  return result; // 返回action,供其他中介軟體處理
}

再看看吧,logger是middlewaremiddlewareAPI相當於store,所以可以在注4呼叫store.getState()方法。

middlewares.map返回值是一個數組,賦值給chain,陣列的每一項是函式。即中介軟體(logger)執行後返回的函式(fn = next => action => {})。

然後增強dispatch。

dispatch = compose(...chain)(store.dispatch)

剛說的chain中的每一項是個陣列,並且compose的作用就是從右到左執行它的函式引數。所以需要執行fn = next => action => {}這個函式,這個函式的返回值還是一個函式(爽不爽!!!,函式是這樣的:fn = action => {},這個函式能接受action作為引數,當然是dispatch了)。對於fn = next => action => {}傳入store.dispatch,所以next就是store.dispatch(上文也提到)。然後將返回的函式next再傳遞給倒數第二個引數(從右往左),然後一直往左,直到第一個。想象一下:

chain = [returnedLogger1, returnedLogger2, returnedLogger3];

首先store.dispatch傳遞給returnedLogger3,返回值是一個函式,fn = action => {},能接受action作為引數,其實際上還是store.dispatch。所以就是store.dispatch一直作為引數從右向左傳遞,直到compose最左邊的函式引數執行完畢,返回的store.dispatch賦值給最初的dispatch。666666版本的dispatch出現了!!!

然後就是applyMiddleware.js內層函式的返回值了。

return {
  ...store,
  dispatch
}

...store表示解構store這個物件(其中有dispatch,subscribe,getState等),其中dispatch會被增強版本的dispatch覆蓋。

至此,applyMiddleware.js分析完畢。

下面是一個測試:

const logger1 = store => next => action => {
  console.log(111);
  let result = next(action);
  console.log(222);
  return result;
}

const logger2 = store => next => action => {
  console.log(111111);
  let result = next(action);
  console.log(222222);
  return result;
} 

const logger3 = store => next => action => {
  console.log(111111111);
  let result = next(action);
  console.log(222222222);
  return result;
} 

const configureStore = (preloadState={aaa: 'aaaa'}) => {
  const store = createStore(
    reducers,
    preloadState,
    compose(
      applyMiddleware(thunk, logger1, logger2, logger3),
      window.devToolsExtension ? window.devToolsExtension() : f => f
    )
  )
 return store;
}

applyMiddleware(thunk, logger1, logger2, logger3),這種形式,打印出的結果是:

111, 111111, 111111111, 222222222, 222222, 222

combineReducers

export default 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') { // 小的reducer肯定是function啊
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  // 忽略中間這些異常處理吧,比如preloadState中的key在reducers(引數reducer,是一個物件)中不存在。
  return function combination(state = {}, action) { // 注5
    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
  }
}

一般而言,finalReducersreducers是相同的(見上述程式碼)。從異常處理下面開始分析。finalReducerKeys是一個數組。

// 例子reducers
export default combineReducers({
  postByBookName,
  selectedBookName,
  user
})

比如上面這個reducers,finalReducerKeys的值是:

["postByBookName","selectedBookName","user"]

由於在createStore的時候傳入了大的reducer,所以store.dispatch一個action的時候大的reducer會處理這個action
createStore.js中有兩行程式碼:

let currentReducer = reducer
let currentState = preloadedState
// 下面這行程式碼在store.dispatch函式中
currentState = currentReducer(currentState, action)

反映在上述程式碼就是執行注5的函式。

return function combination(state = {}, action) { // 注5

關鍵是這5行程式碼,以上述例子reducers其中的selectedBookName為例。

const key = finalReducerKeys[i] 
const reducer = finalReducers[key] 
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)

nextState[key] = nextStateForKey

key是字串selectedBookName
reducerselectedBookName對應的reducer。我的是:

const selectedBookName = (state = 'java', action) => {
  switch (action.type) {
    case SELECT_BOOKNAME:
      return action.payload.text;
    default:
      return state;
  }
}
export default selectedBookName;

previousStateForKeyundefined
nextStateForKeyselectedBookName函式執行的結果,如果匹配上了返回action.payload.text,假如是”react”。
nextState["selectedBookName"] = "react"

此時由於previousStateForKey不等於nextStateForKey,所以hasChanged的值為true。返回nextState

至此,combineReducers.js分析完畢。

bindActionCreators

這是個輔助函式。比如:

const test = () => {
  type: 'test',
}

以前我們的用法是dispatch(test()),使用了這個輔助函式,我們只需要test()。就是少寫了dispatch這麼回事。一般情況下我不會用到。

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

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

  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)
    } else {
      warning(`bindActionCreators expected a function actionCreator for key '${key}', instead received type '${typeof actionCreator}'.`)
    }
  }
  return boundActionCreators
}

上述原始碼分為兩種情況,一種是typeof actionCreator === 'function',就是我舉例的那種。一種是actionCreator是一個物件。通常在這種情況出現:

import * as Actions from '../actions'

這樣Actions是一個物件,每個import出的函式就是Actions的一個屬性值。然後向一種一樣的處理。通常我不會使用這個輔助函式,當我使用react-redux的時候,我可能會用到。

import * as CounterActions from '../actions'

const mapStateToProps = (state) => ({
  counter: state.counter
})

function mapDispatchToProps(dispatch) {
  return bindActionCreators(CounterActions, dispatch)
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter)

然後我直接呼叫CounterActions.myAction()就直接dispatch了。

結語

花了一晚上時間,梳理了redux的結構,加深了理解。對原始碼進行了細緻的分析。如果有更多的想要和我交流,可以加我:

QQ: 1227620310
微信: a127620310