redux相關原理解析

redux概念

應用的全部狀態以一個對象的形式存儲在惟一的store裏面,咱們能夠把這個存儲狀態的對象叫狀態樹,而更改對象樹的惟一方式就是emit一個action(這個action是一個描述發生了什麼的對象),爲了把這個action轉換爲對象樹裏面的某個值,就須要提供一個reducer, 因此簡單來講,一個redux程序包括一個store、一個reducer和多個action。node

簡單例子, 也是一個基本使用

import { createStore } from 'redux';

export const reducer = (state = {}, action) => {
    switch (action.type) {
        case 'EXAMPLE_TEST': {
            // balabala
            return {text: 'ready go'}
        }
        default:{
            return state;
        }
    }
};

const initialState = {}; // 初始state
const store = createStore(reducer, initialState);

store.dispatch({type: 'EXAMPLE_TEST', data: {}});
store.subscribe(() => {
   const stateObj = store.getState();
   
   if (stateObj.xxx...) {
     // state樹更改了, balabala
   }
})
console.log(store.getState())

上面涉及到的相關api邏輯詳細解析(node_modules/redux/src..)

createStore

export default function createStore(reducer, preloadedState, enhancer) {
    ...
    
    dispatch({ type: ActionTypes.INIT })
    
    return {
        dispatch,
        subscribe,
        getState,
        replaceReducer,
        [$$observable]: observable
    }
}

從參數能夠看出,它是action和reducer的粘合劑,調用一下dispatch生出一個初始化後的對象樹(如何生成,見下面的dispatch解釋),而後向外曝露出一些功能函數,具體功能函數見下面說明。react

subscribe
function subscribe(listener) {
    if (typeof listener !== 'function') {
        throw new Error('Expected listener to be a      function.')
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

把經過store.subscribe(()=>{})添加的listener放到nextListeners數據裏,並返回一個unsubscribe函數,用於取消訂閱,這裏面屢次出現的函數:ensureCanMutateNextListeners做用就是確保currentListeners和nextListeners不指向同一個數組,把將要執行的listeners和之後要執行的listeners區分開,這樣能夠避免掉listener正在被執行的時候,忽然取消訂閱的問題。webpack

getState
function getState() {
    return currentState
}

做用就是返回當前的狀態樹對象git

[$$observable]: observable
function observable() {
    const outerSubscribe = subscribe
    return {
        subscribe(observer) {
            if (typeof observer !== 'object') {
                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
      }
    }
  }

把store針對state樹變動可observable,以支持redux和RxJS, XStream and Most.js等庫的聯合使用,須要再詳細說明的是$$observable,它來自於symbol-observable這個npm包,這個包的做用就是使一個對象observable, 而且避免掉observable對象調用完error, complete以後重複調用以及unsubscribe以後再調用next, error, complete函數,具體使用見README.md:https://github.com/benlesh/sy...github

dispatch
function dispatch(action) {
    ...... // 省略掉了非關鍵代碼
    
    try {
        isDispatching = true
        currentState = currentReducer(currentState, action)
    } finally {
        isDispatching = false
    }
    
    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i]
        listener()
    }

    return action
  }

dispatch一個action,就是把當前state對象和action傳遞給傳給store的reducer做爲參數,而後reducer返回的新對象就是新的state樹,生成新的states樹以後,再執行全部經過store.subscribe函數註冊的listener, 註冊方式見上面subscribe說明,最後,咱們回到上面createStore遺留的dispatch({ type: ActionTypes.INIT })部分,就很明瞭了,就是生成初始的state樹。web

replaceReducer
function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
        throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.INIT })
  }

這個函數主要是替代creatStore傳進來的reducer, 使用場景如:代碼邏輯裏有代碼分割邏輯,須要動態加載reducer; 或者爲了實現redux的熱加載, 如:npm

if (module.hot) {
    module.hot.accept('../reducers', () => {
        const nextRootReducer = require('../reducers').default;
            store.replaceReducer(nextRootReducer);
        });
    }

其中module.hot是webpack經過HotModuleReplacementPlugin開啓了模塊熱替換以後賦予module的hot屬性。編程

上面說了基本的redux使用原理,可是在實際使用中,咱們可能會遇到以下問題

  1. 若是項目很複雜,把全部的action處理都放到一個reducer下,那reducer的case得寫多少,有可能會致使單個文件過大,代碼很差維護?
  2. redux有沒有調試工具,以便於咱們在開發階段進行調試,若是有,怎麼用?
  3. 項目中每一個都少不了網絡請求,那麼咱們每一個請求都要判斷成功,失敗,和請求中三個狀況的dispatch,那麼每一個請求都這麼寫,顯的累贅, 提取公共部分到公共函數就能解決嗎?
  4. redux應用到項目中,每一個組件都要subscribe一下,而後判斷本身關注的state是否是變了嗎?

問題一:

redux提供一個combineReducers工具,容許咱們寫多個小reducer,而後經過combineReducers組合成一個root reducer,也就是一個大reducer,源碼以下:redux

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 (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  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)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

給combineReducers傳入一個reducer對象,而後返回一個reducer函數,參數裏面每一個value須要是一個小reducer函數, 這些小的reducer能夠分佈到不一樣的文件裏面,那麼觸類旁通,一些最小的reducer能夠經過combineReducers組裝成中等的reducer, 而後這些中等的reducer又能夠combineReducers成最大的reducer:api

export default combineReducers({
    a: combineReducers(...),
    b: combineReducers(...),
    c: combineReducers(...)
});

問題二:

已經有redux-logger相似的工具了,咱們看它的定義:

// 默認的logger
export const logger: Redux.Middleware;
// 自定義logger
export function createLogger(options?: ReduxLoggerOptions): Redux.Middleware;
// Redux.Middleware
export interface Middleware {
  <S>(api: MiddlewareAPI<S>): (next: Dispatch<S>) => Dispatch<S>;
}

從上面能夠看出返回類型都是Redux.Middleware,這代表它是以redux中間件的形式融入到redux中, 這些中間件實現方式都是統一的,接收一個MiddlewareAPI參數,而後返回一個function(這個function接收一個dispatch,而後將dispatch結果返回)。這些中間件注入到redux中須要藉助redux提供的applyMiddleware函數:

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    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
    }
  }
}

applyMiddleware接收多箇中間件,而後返回一個函數,這個函數參數是一個createStore, 函數邏輯是調用createSotre拿到最基本的store,而後遍歷全部的中間件,給其傳入MiddlewareAPI,拿到中間件執行結果(返回是一個函數,是Redux.Middleware類型),而後經過compose把全部中間件給柯里化一下,把基本的store.dispatch武裝成一個強大的dispatch,這樣每次dispatch(action)就會走這些中間件了,也就能夠觀察action結果變化,其中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)))
}

結合中間件場景:dispatch = compose(...chain)(store.dispatch)能夠理解爲以傳入的中間件順序倒序爲基準,依次將基礎的store.dispatch傳給各個中間件的返回函數。
綜上,中間件落實到項目中使用就是:

import { createStore, applyMiddleware } from 'redux';

const bigCreateStore = applyMiddleware(middleware1, middleware2, ...)(createStore)

bigCreateStore.dispatch(action)

還有不少其餘中間件,好比你action是個function的話就要藉助redux-thunk中間件。

問題三

咱們把它提取到工具類公共函數中,能夠是能夠,可是整個項目代碼看起來不優雅,咱們能夠向redux看齊,以一箇中間件的形式落地這個異步請求處理,咱們能夠本身實現一箇中間件,也可使用現成的, 好比:https://github.com/agraboso/r...

問題四

整個項目中處處充滿着subscribe,並且一旦整個對象樹更改了,全部組件的subscribe都要執行state變化判斷,看是否從新render,這樣容易作成無用功,咱們能夠藉助已有的工具:react-redux, 給咱們解決掉這個判斷處理麻煩,使用事例:

import { Provider, connect } from 'react-redux';

class App extends Component {
    render () {
        return (
            <div>app內容</div>
        );
    }
}
export default 

export default class RootComponent extends Component {
    render () {
        return (
            <Provider store={store}>
                connect(state => ({
                    a0: state.a.a0,
                    b0: state.b.b0,
                    c0: state.c.c0
                }), {
                    ...actions
                })(App)
            </Provider>
        )
    }
}

從上面使用上看,主要是Provider Class和connect函數兩個東西。經過connect包裝以後的Component和Provider經過context進行通訊,分開來看,先說Provider:

Provider.propTypes = {
    store: storeShape.isRequired,
    children: PropTypes.element.isRequired,
}
Provider.childContextTypes = {
    [storeKey]: storeShape.isRequired,
    [subscriptionKey]: subscriptionShape,
}

其源碼中能夠看出它接收兩個prop,一個是store, 一個是children。另外子孫組件想要和Provider通訊,使用store和subscriptionKey能力,就得經過context[storeKey]和context[subscriptionKey]使用,同時子孫組件也要提供可使用context對象的依據(好比能夠看connect函數源代碼裏面的高階組件):

const contextTypes = {
    [storeKey]: storeShape,
    [subscriptionKey]: subscriptionShape,
  }

再看connect:

function connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    {
      pure = true,
      areStatesEqual = strictEqual,
      areOwnPropsEqual = shallowEqual,
      areStatePropsEqual = shallowEqual,
      areMergedPropsEqual = shallowEqual,
      ...extraOptions
    } = {}
  ) {
    const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps')
    const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps')
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

    return connectHOC(selectorFactory, {
      // used in error messages
      methodName: 'connect',

       // used to compute Connect's displayName from the wrapped component's displayName.
      getDisplayName: name => `Connect(${name})`,

      // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
      shouldHandleStateChanges: Boolean(mapStateToProps),

      // passed through to selectorFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,

      // any extra options args can override defaults of connect or connectAdvanced
      ...extraOptions
    })
  }

connect的結果就是返回一個高階組件,這個高階組件中的兩個重要函數就是:

initSelector() {
    const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
    this.selector = makeSelectorStateful(sourceSelector, this.store)
    this.selector.run(this.props)
}

initSubscription() {
    if (!shouldHandleStateChanges) return
    
    const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
    this.subscription = new Subscription(this.store, parentSub, this.onStateChange.bind(this))
    this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription)
}

initSelector判斷state狀態樹或者actions是否更改,來決定被包裹組件的新props,關鍵代碼見:

const nextProps = sourceSelector(store.getState(), props)
    if (nextProps !== selector.props || selector.error)  {
        selector.shouldComponentUpdate = true
        selector.props = nextProps
        selector.error = null
}
...
render() {
    ...
    return createElement(WrappedComponent, this.addExtraProps(selector.props))
    ...
}

initSubscription的主要做用就是監聽store的state對象樹是否變化,若是變化,就執行以下代碼:

onStateChange() {
    this.selector.run(this.props) // 獲取最新的props

    if (!this.selector.shouldComponentUpdate) {
      this.notifyNestedSubs()
    } else {
      this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
      this.setState(dummyState)
    }
  }

也就是說先獲取最新的props, 這個props裏面包括states,而後判斷是否須要從新渲染,若是須要,則觸發setState,引發高階組件react生命週期的執行,再看this.notifyNestedSubs()這句,若是被包裹組件訂閱了state變化,那麼會依次執行全部的listener,被包裹組件若是若是想訂閱,須要藉助context,由於高階組件裏面定義了以下代碼:

const childContextTypes = {
    [subscriptionKey]: subscriptionShape,
}

以上就是問題四的相關內容,使用過程當中須要注意的是connect函數的第二個參數也就是mapDispatchToProps能夠是個actions對象,也能夠是一個方法,我習慣用對象,寫起來方便,其實底層最終都是同樣的,只是若是用的是對象的話,react-redux內部會調用

bindActionCreators(mapDispatchToProps, dispatch)

把對象處理成新對象,這個新對象的values都是

(...args) => dispatch(actionCreator(...args))

因此就能夠知足咱們項目中this.props.xxx(data)來dispatch一個action。

react-redux中高階組件是如何判斷props更改,而後決定是否從新render的?

高階組件的props來源兩部分,分別是state對橡樹裏面的對象和actions, 詳細看react-redux包下面的src/connect/selectorFactory.js代碼片斷

function handleSubsequentCalls(nextState, nextOwnProps) {
    const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
    const stateChanged = !areStatesEqual(nextState, state)
    state = nextState
    ownProps = nextOwnProps

    if (propsChanged && stateChanged) return handleNewPropsAndNewState()
    if (propsChanged) return handleNewProps()
    if (stateChanged) return handleNewState()
    return mergedProps
  }

總結

redux源代碼處處充滿着函數式編程以及調用compose柯里化,源代碼量很少,簡短乾淨,看了很舒服,上面的整個分析一句的redux版本是3.7.2,react-redux版本5.1.1, 若是後續版本相關邏輯更改了請見諒。

相關文章
相關標籤/搜索