redux 和 react-redux 部分原始碼閱讀

liushanga發表於2018-12-29

從原始碼的入口檔案發現,其實 redux 最終就只是匯出了一個物件,物件中有幾個方法,程式碼如下:

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

所以重點分析幾個方法:

createStore 方法

方法中定義的一些變數:

let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
複製程式碼

這些變數會被 dispatch 或者別的方法引用,從而形成閉包。這些變數不會被釋放。

建立 srore 的方法最終返回的是一個物件。物件中含有比較重要的方法dispatch,subscribe,getState

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

其中 createStore 的第三個引數是應用中介軟體來做一些增強操作的。

if (typeof enhancer !== 'undefined') { // 如果增強方法存在就對 createStore 進行增強
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
}
複製程式碼

subscribe 方法

其中 subscribe 用來註冊監聽方法,每一次註冊後會將監聽方法維護到陣列currentListeners中,currentListenerscreateStore 中的一個變數,由於被 subscribe 引用著所以形成了一個閉包。也就是通過閉包來維護狀態。

let currentListeners = []
複製程式碼

dispatch 方法

dispatch 方法用來分發 action, 函式裡面會生成新的 currentState, 會執行所有註冊了的函式。

核心程式碼:

try {
  isDispatching = true
  currentState = currentReducer(currentState, action) // 生成新的 state
} finally {
  isDispatching = false
}

const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
  const listener = listeners[i]
  listener()
} // 遍歷執行註冊函式
複製程式碼

getState

僅僅用來獲得當前的 state:

function getState() {
    return currentState
}
複製程式碼

combineReducers 函式

函式中定義的一些變數,

const finalReducers = {}
const finalReducerKeys = Object.keys(finalReducers)
複製程式碼

這個函式最後返回的是一個函式 combination, 返回的函式中引用了 finalReducersfinalReducerKeys,形成了閉包。

出於業務場景考慮,不同的模組採用不同的 reducer 進行處理,所以 reducer 函式有很多。這些 reducer 會遍歷執行。

每一次 dispatch 一個 action 的時候就會執行

currentState = currentReducer(currentState, action) // 生成新的 state
複製程式碼

這裡的 currentReducer 就是返回的 combination 函式。combination 函式中的核心程式碼:

function combination(state = {}, action) {
    ...
    let hasChanged = false
    // 每一次 reducer 執行的時候都會生成一個新的物件來作為新的 state 
    const nextState = {}
    // 通過 for 迴圈遍歷 reducer 
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      
      // 獲取當前的 state
      const previousStateForKey = state[key]
      
      // 執行相應的 reducer 後會生成新的 state
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      
      // 給新的 state 賦值
      nextState[key] = nextStateForKey
      
      // 如果是一個簡單型別比如 string,number 
      // 如果前後值一樣就不會觸發改變
      // 但如果 state 中某個值是一個物件,
      // 儘管前後物件中的值一樣,但是引用地址變化,還是會觸發改變
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    
    // 所以如果簡單值沒有變化並且沒有物件的引用地址改變就會返回原來的 state
    return hasChanged ? nextState : state
}
複製程式碼

結合 react-redux 中向 redux 訂閱的方法發現

subscribe() {
    const { store } = this.props  // 這裡的 store 是 createStore 方法執行後返回的物件
     
    this.unsubscribe = store.subscribe(() => { // 通過訂閱方法註冊監聽事件
      const newStoreState = store.getState() // 獲取新的 state
    
      if (!this._isMounted) {
        return
      }
    
      // 通過使用函式替代物件傳入 setState 的方式能夠得到元件的 state 和 props 屬性可靠的值。
      this.setState(providerState => {
        // 如果值是一樣的就不會觸發更新
        if (providerState.storeState === newStoreState) {
          return null
        }
    
        return { storeState: newStoreState }
      })
    })
    
    // Actions might have been dispatched between render and mount - handle those
    const postMountStoreState = store.getState()
    if (postMountStoreState !== this.state.storeState) {
      this.setState({ storeState: postMountStoreState })
    }
}
複製程式碼

在註冊的 listen 方法中會發現如果最 新的state和原來的state一樣 就不會觸發 setState 方法的執行,從而就不會觸發 render

applyMiddleware 使用中介軟體

原始碼:

export default function applyMiddleware(...middlewares) {
    return createStore => (...args) => { // 接收 createStore 函式作為引數
        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)
        } // 中介軟體函式接收的 API 引數,能夠獲取到當前的 state 和 createStore 函式的引數
        // 所以這裡就向中介軟體函式中傳遞了引數
        const chain = middlewares.map(middleware => middleware(middlewareAPI))
        // 通過函式組合生成一個新的 dispatch 函式
        dispatch = compose(...chain)(store.dispatch)
        
        return {
          ...store,
          dispatch
        } // 這裡返回的是最後生成的 store,相比不使用中介軟體的區別是對 dispatch 進行了增強。
    }
}
複製程式碼

結合 createStore 中的原始碼:

return enhancer(createStore)(reducer, preloadedState)
複製程式碼

所以上面 applyMiddleware 中返回的函式就是這裡的 enhancer 方法,接收 createStore 作為引數。

(reducer, preloadedState) 對應著中介軟體中的 (...args)

react-redux

react-redux 通過提供 Provider 元件將 store 和整個應用中的元件聯絡起來。確保整個元件都可以獲得 store, 這是通過 Context 來實現的。

Provider 元件最終渲染的元件:

render() {
    const Context = this.props.context || ReactReduxContext

    return (
      <Context.Provider value={this.state}>
        {this.props.children}
      </Context.Provider>
    )
}
複製程式碼

其中 state 的定義如下:

const { store } = props
this.state = {
  storeState: store.getState(),
  store
}
複製程式碼

所以 Provider 給應用提供 store 的寫法如下,屬性名必須是 store

<Provider store={store}>
  <Router />
</Provider>
複製程式碼

redux-thunk

redux-thunk 是一箇中介軟體,直接看中介軟體的原始碼是絕對不可能看明白的

中介軟體不是一個完整的個體。它是為了豐富或者擴充套件某個模組而出現的,其中會呼叫一些原來的模組的方法,所以如果不看源模組的對應的方法實現,根本無法理解。

所以要想看懂一箇中介軟體,必須結合源模組的程式碼一起看。

Thunk 函式的含義和用法

JavaScript 語言是傳值呼叫,它的 Thunk 函式含義有所不同。在 JavaScript 語言中,Thunk 函式替換的不是表示式,而是多引數函式,將其替換成單引數的版本,且只接受回撥函式作為引數

如何讓 dispatch 分發一個函式,也就是 action creator??

dispatch 的引數只能是一個普通的物件,如果要讓引數是一個函式,需要使用中介軟體 redux-thunk

設計思想就是一種面向切面程式設計AOP,對函式行為的增強,也是裝飾模式的使用

redux-thunk 原始碼:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;
複製程式碼

如果只是用了 thunk,那麼最終增強版的 dispatch 就是

action => {
    // 當 dispatch 引數是一個函式的時候執行這裡
    if (typeof action === 'function') { 
      // 這裡的 dispatch 就是最原始的 dispatch
      // 所以 action 函式中可以直接使用引數 dispatch 和 getState 函式
      return action(dispatch, getState, extraArgument);
    }

    return next(action); // 這裡的 next 是 store.dispatch
}
複製程式碼

非同步操作帶程式碼

非同步操作如果使用 action creator, 則至少要送出兩個 Action:

  • 使用者觸發第一個 Action,這個跟同步操作一樣,沒有問題;
  • action creator 函式中送出第二個 Action

程式碼例項:

handleClick = () => {
    const { dispatch } = this.props
    dispatch(this.action); // 發出第一個 action(函式)
}

action = (dispatch, getState) => setTimeout(() => {
    dispatch({ type: 'REQUESTSTART' })
}, 1000) // 發出第二個 action(普通物件)
複製程式碼

思考

非同步程式碼的處理一定要使用 redux-thunk嗎?

非也。在觸發含有非同步程式碼的函式執行時,把 dispatch 函式作為一個引數傳給函式,然後這個非同步函式裡面在合適的時機呼叫 dispatch 發出 action 就行。

上面的非同步程式碼可改寫如下:

handleClick = () => {
    const { dispatch } = this.props
    this.action(dispatch);
}

action = dispatch => setTimeout(() => {
    dispatch({ type: 'REQUESTSTART' })
}, 1000)
複製程式碼

不過相比 redux-thunk 有個缺陷就是不能獲取 getState 這個方法。

使用示例

使用 redux 演示程式碼

相關文章