React 全家桶之 redux

redux 流程圖

redux 的使用

先看一下目錄結構

components/counter.js:創建store

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import reducers from '../reducers';
const store = createStore(reducers, applyMiddleware(thunk))

export default store;

reducers/counter.js:構建組件初始狀態;創建純函數reducer;接收action,返回全新的state。

const initialState = {
    num: 0
}

const reducer = (state = initialState, action) => {
    const { type, data } = action;
    switch (type) {
        case 'increment':
            return {
                num: state.num + data
            }
        case 'decrement':
            return {
                num: state.num - data
            }
        default:
            return state;
    }
}

export default reducer;

reducers/index.js:組合多個 reducer,通過 action.type 將 action 分發到具體的reducer中處理

import { combineReducers } from 'redux';
import counter from './counter';
export default combineReducers({
    counter
})

actions/counter.js:actionCreator,產出 action 的地方;返回值爲一個擁有 type 字段的 object對象,或者是一個函數(需要加入redux-thunk 中間件予以支持,後面會講到)

 import { bindActionCreators } from 'redux';
 import store from '../store';

 const actionCreator = {
    increment: () => {
        return {
            type: 'increment',
            data: 1
        }
    },
    decrement: () => {
        return (dispatch, getState) => {
            setTimeout(() => {
                dispatch({
                    type: 'decrement',
                    data: 2
                })
            }, 1000)
        }
    },
    reset: () => {
        return {
            type: 'reset'
        }
    }
}

actionCreator.reset = bindActionCreators(actionCreator.reset, store.dispatch);

export default actionCreator

components/counter.js:view層,執行 dispatch -> 觸發 action -> action 經過相應的 reducer 處理後,返回全新的 state -> 執行 store.subscribe 註冊的監聽函數 -> 監聽函數中 setState 更新狀態 -> 觸發 react 更新機制 -> 組件更新

import React from 'react';
import store from '../store';
import actionCreator from '../actions/counter'

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      num: store.getState().counter.num
    }
    store.subscribe(() => {
      const state = store.getState();
      this.setState({
        num: state.counter.num
      })
    })
  }
  increment = () => {
    store.dispatch(actionCreator.increment())
  }
  decrement = () => {
    store.dispatch(actionCreator.decrement())
  }
  reset = () => {
    actionCreator.reset()
  }
  render() {
    const { num } = this.state;
    return (
      <div className="App">
        {
          num
        }
        <div></div>
        <button onClick={ this.increment }>+</button>
        <button onClick={ this.decrement }>-</button>
        <button onClick={ this.reset }>reset</button>
      </div>
    );
  }

}

export default Counter;

redux 原理解析

整個過程用到了幾個重要的方法:

  • createStore:創建 store
  • store.dispatch:觸發 action
  • store.subscribe:註冊監聽函數,dispatch中會調用所有註冊的監聽函數
  • store.getState:返回當前 state
  • combineReducers:組合多個 reducer 成一個,根據 action.type 分發 action 到對應的 reducer 中處理
  • bindActionCreators:用 dispatch 封裝 actionCreator,這樣在組件中直接調用 actionCreator 方法就能執行 dispatch,而不需要在組件中顯示的調用 dispatch
  • applyMiddleware:利用中間件,增強 dispatch 的功能

下面分別大致說一下每個方法的實現過程

createStore

createStore 接收三個參數 (reducer, preloadedState, enhancer),reducer 好理解,preloadedState 是初始狀態,很少用到,enhancer 是增強器,用來增強 redux。

上面的代碼中我是這樣寫的 createStore(reducers, applyMiddleware(thunk)),enhancer寫在了第二個參數上,第三個參數沒有傳,之所以能生效是因爲下面這個判斷

  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
    preloadedState = undefined
  }

常見的增強器有 redux-thunk、redux-saga,需要使用 applyMiddleWare 處理之後纔是合格的 enhancer。

該函數內部定義了這麼幾個重要的變量

let currentState = preloadedState //從函數createStore第二個參數preloadedState獲得
let currentReducer = reducer  //從函數createStore第一個參數reducer獲得
let currentListeners = [] //當前訂閱者列表
let nextListeners = currentListeners //新的訂閱者列表
let isDispatching = false

createStore 執行完會返回這麼幾個東西:

{
    dispatch: dispatch as Dispatch<A>,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
 }

dispatch

if (!isPlainObject(action)) {
  // dispatch 的入參必須是一個簡單的 Object 對象
  throw new Error(
    'Actions must be plain objects. ' +
      'Use custom middleware for async actions.'
  )
}

if (typeof action.type === 'undefined') {
  // 並且這個 Object 需要具有 type 屬性
  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.')
}

try {
  isDispatching = true  // 可以理解爲線程鎖,防止兩個action同時執行
  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()    // 執行所有的監聽函數
}

subscribe

nextListeners.push(listener)

// 返回解綁函數
return function unsubscribe() {
  ...
  const index = nextListeners.indexOf(listener)
  nextListeners.splice(index, 1)
}

getState

return currentState as S

combineReducers

它的作用是用來合併多個 reducer,最終會返回一個函數代替所有的 reducer。在 dispatch 後,這個合併後的 combination 會遍歷所有的 reducer 並執行它們,拿到最新的 state。然後與原來的 state 對比看是否是同一個引用,如果是就返回老的 state,否則返回新的 state。

先將入參 reducers 拷貝到 finalReducers 中
const finalReducerKeys = Object.keys(finalReducers)
return function combination(state = {}, action) { 
    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)

    nextState[key] = nextStateForKey
    hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
}

bindActionCreators

經過該方法封裝後的 actionCreator 具備了 dispatch 能力,所以 actionCreator 直接調用就可以觸發 action,不用在組件中顯示的調用 dispatch。如 actions/counter.js 中的 reset,在組件中直接調用 counter.reset() 就可以觸發 dispatch,而不用這麼寫 store.dispatch(counter.reset())

function bindActionCreator<A extends AnyAction = AnyAction>(
  actionCreator: ActionCreator<A>,
  dispatch: Dispatch
) {
  return function(this: any, ...args: any[]) {
    return dispatch(actionCreator.apply(this, args))
  }
}
export default function bindActionCreators(
  actionCreators: ActionCreator<any> | ActionCreatorsMapObject,
  dispatch: Dispatch
) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  /***********容錯**********/

  const boundActionCreators: ActionCreatorsMapObject = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

applyMiddleware

在 dispatch 函數中我們提到,它的入參必須是一個簡單的 Object 對象,而且還需要有 type 屬性。如 actions/counter.js 中的 increment,它的返回值就是帶有 type 字段的 Object 對象。

而 decrement 這個 actionCreator 的返回值卻是一個函數,最後也順利的觸發了 action,更新了試圖。這是爲什麼呢?如果依據 dispatch 的源碼來看,這肯定會報錯纔對啊?

原因就是我們使用了 redux-thunk 這個中間件增強了 dispatch 的功能,使其具備了接收函數作爲入參的能力。

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
    }
  }
}
  •  增強器 enhancer 是 applyMiddleware 方法的返回值,這個 enhancer 主要分爲一下幾步:
  • 入參是 createStore,通過它創建一個 store
  • 定義 middlewareAPI,所有的中間件都需要實現 getState、dispatch 這兩個方法
  • middlewares 調用 Array.prototype.map 進行改造,存放在 chain
  • 用compose整合chain數組,並賦值給dispatch
  • 將新的dispatch替換原先的store.dispatch

這樣看還有有些懵逼,我們來看一下大名鼎鼎的 redux-thunk 是怎麼實現的。看懂了它再回頭去看 applyMiddleware 就豁然開朗了

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 應該是這個樣子的

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

它的入參是一個簡單 Object,且擁有 dispatch 和 getState 兩個屬性。我們現在再把這個 thunk 帶回到 applyMiddleware 方法中的 chain 那一步

const chain = middlewares.map(middleware => middleware(middlewareAPI))

執行 thunk 可以得到如下結果,是一個函數,它的入參是 next。返回值也是一個函數,入參是 action。爲了方便理解,我們假設使用了兩個中間件

// thunk(middlewareAPI)
const result1 = next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
};
const result2 = 同上
// 那麼上一步的 chain 就是這個樣子
chain = [result1, result2]

現在我們得到了 chain 的模樣,接着把 chain 帶入到 applyMiddleware 中

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

compose 函數是用來組合多個函數的,把一個函數的執行結果作爲下一個函數的入參。我們來看下源碼

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

那麼 chain 經過 compose 處理之後得到的應該是一個函數

const composeRst = (args) => result1(result2(args))

走到這裏,我們再回頭看 result1 的代碼,就能夠知道它的入參 next 其實就是下一個中間件。我們接着往下執行

dispatch = compose(...chain)(store.dispatch)
等價於
dispatch = composeRst(store.dispatch)
等價於
dispatch = result1(result2(store.dispatch))
等價於
dispatch = action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return result2(action);
}

也就是說經過 compose(...chain)(store.dispatch) 一頓操作猛如虎,最終得到的其實就是一個函數,這個函數的入參是一個 action。這不就是 dispatch 的功能嗎!!!

但是比原始的 dispatch 多了個 if 判斷,action 原本只能是一個帶有 type 屬性的簡單 Object,現在可以傳 function 了,然後把 dispatch、getState 傳入了這個 function。

利用這一點我們就可以在 actionCreator 中返回一個異步函數去後端獲取數據,然後在異步函數的回調中調用 dispatch 更新狀態,如文章開頭 actions/counter.js 中的 decrement 這個 actionCreator,是不是覺得很巧妙。鼎鼎大名的 redux-thunk 其實就那麼幾行代碼。

整個 redux 的使用及原理講完了,但是代碼這麼寫一點也不夠優雅。通常在 react 項目中使用 redux,我們會借用 react-redux 做爲橋樑,更加優雅的使用 redux。下一篇講解 react-redux 的使用和原理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章