react-redux的Provider和connect

草榴社群發表於2017-10-21

react-redux簡介

redux是一個資料管理框架,而react-redux是專門針對react開發的一個外掛。react-redux提供了2個API,Provider和connect。本來打算在一篇文章同時講解2個API的實現,不過看了一下connect的原始碼,368行,還是分開解析吧。

本文帶領大家分析Provider的核心程式碼。

如何使用Provider

我們先了解在react專案中是如何使用Provider。

import { Provider } from 'react-redux';
    import configureStore from './store/configureStore';

    const store = configureStore();
    ReactDOM.render((
        <Provider store={store}>
            
        </Provider>),
        document.getElementById('root')
    );複製程式碼

上面的程式碼可以看出,使用Provider分為下面幾個步驟:

1、匯入Provider 這裡跟小白分享一個小知識,你可以看到Provider加了個大括號,而第二個import configureStore沒有加大括號,這是因為react-redux的檔案中沒有指定default輸出。如果指定了export default,則不需要加大括號,注意一個js檔案只能有一個default。

import { Provider } from 'react-redux';複製程式碼

2、將store作為引數傳入Provider。

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

Provider原始碼

import { Component, Children } from 'react'
    import PropTypes from 'prop-types'
    import storeShape from '../utils/storeShape'
    import warning from '../utils/warning'
    
    let didWarnAboutReceivingStore = false
    function warnAboutReceivingStore() {
      if (didWarnAboutReceivingStore) {
        return
      }
      didWarnAboutReceivingStore = true
    
      warning(
        '<Provider> does not support changing `store` on the fly. ' +
        'It is most likely that you see this error because you updated to ' +
        'Redux 2.x and React Redux 2.x which no longer hot reload reducers ' +
        'automatically. See https://github.com/reactjs/react-redux/releases/' +
        'tag/v2.0.0 for the migration instructions.'
      )
    }
    
    export default class Provider extends Component {
      getChildContext() {
        return { store: this.store }
      }
    
      constructor(props, context) {
        super(props, context)
        this.store = props.store
      }
    
      render() {
        return Children.only(this.props.children)
      }
    }
    
    if (process.env.NODE_ENV !== 'production') {
      Provider.prototype.componentWillReceiveProps = function (nextProps) {
        const { store } = this
        const { store: nextStore } = nextProps
    
        if (store !== nextStore) {
          warnAboutReceivingStore()
        }
      }
    }
    
    Provider.propTypes = {
      store: storeShape.isRequired,
      children: PropTypes.element.isRequired
    }
    Provider.childContextTypes = {
      store: storeShape.isRequired
    }複製程式碼

Provider原始碼解析

Provider只有一個引數,非常簡單,程式碼也僅有55行。

1、Provider是一個react元件

import { Component, Children } from 'react'
    import PropTypes from 'prop-types'
    import storeShape from '../utils/storeShape'
    import warning from '../utils/warning'

    export default class Provider extends Component {
      getChildContext() {
        return { store: this.store }
      }
    
      constructor(props, context) {
        super(props, context)
        this.store = props.store
      }
    
      render() {
        return Children.only(this.props.children)
      }
    }複製程式碼

Provider元件寫了3個方法,getChildContext、constructor、render。

constructor是構造方法,this.store = props.store中的this表示當前的元件。在建構函式定義this.store的作用是為了能夠在getChildContext方法中讀取到store。

你最不熟悉的可能就是getChildContext,翻譯過來就是上下文。什麼意思呢?又有什麼用呢?我們看到getChildContext方法是返回store。接著,就看不到store哪去了。

最後執行render渲染,返回一個react子元素。Children.only是react提供的方法,this.props.children表示的是隻有一個root的元素。

2、給Provider元件設定propTypes驗證。storeShape是一個封裝的方法。

Provider.propTypes = {
        store: storeShape.isRequired,
        children: PropTypes.element.isRequired
    }

  
    //storeShape
    import PropTypes from 'prop-types'
    
    export default PropTypes.shape({
      subscribe: PropTypes.func.isRequired,
      dispatch: PropTypes.func.isRequired,
      getState: PropTypes.func.isRequired
    })複製程式碼

3、驗證childContextTypes 它的作用就是讓Provider下面的子元件能夠訪問到store。 詳細解釋和用法看 react關於context的介紹

Provider.childContextTypes = {
      store: storeShape.isRequired
    }複製程式碼

4、node執行環境判斷 如果不是生產環境,也就是在開發環境中,實現componentWillReceiveProps()。

if (process.env.NODE_ENV !== 'production') {
      Provider.prototype.componentWillReceiveProps = function (nextProps) {
        const { store } = this
        const { store: nextStore } = nextProps
    
        if (store !== nextStore) {
          warnAboutReceivingStore()
        }
      }
    }複製程式碼

其實也可以把這段程式碼寫到Provider元件內部去。

他的作用是當接收到新的props的時候,如果是在開發環境下,就判斷當前的store和下一個store是不是不相等,如果是,就執行warnAboutReceivingStore()。

export default class Provider extends Component {
      
      componentWillReceiveProps(nextProps) {
        if (process.env.NODE_ENV !== 'production') {
          const { store } = this
          const { store: nextStore } = nextProps
    
          if (store !== nextStore) {
            warnAboutReceivingStore()
          }
        }
      }
      
      render() {
        return Children.only(this.props.children)
      }
    }複製程式碼

5、warnAboutReceivingStore的作用。 上面說到執行了warnAboutReceivingStore,那麼warnAboutReceivingStore的作用是什麼呢?

let didWarnAboutReceivingStore = false
        function warnAboutReceivingStore() {
          if (didWarnAboutReceivingStore) {
            return
          }
          didWarnAboutReceivingStore = true
          
          warning(
        '<Provider> does not support changing `store` on the fly. ' +
        'It is most likely that you see this error because you updated to ' +
        'Redux 2.x and React Redux 2.x which no longer hot reload reducers ' +
        'automatically. See https://github.com/reactjs/react-redux/releases/' +
        'tag/v2.0.0 for the migration instructions.'
      )複製程式碼

didWarnAboutReceivingStore是一個開關的作用,預設是false,也就是不執行warning操作。當props更新的時候,執行了warnAboutReceivingStore(),如果didWarnAboutReceivingStore為true,則return,否則就將didWarnAboutReceivingStore設定為true。然後就會執行warning的警告機制。

這樣做的目的是不允許在componentWillReceiveProps做store的更新操作。

總結

很快就到尾聲了,Provider是一個react元件,提供了一個引數store,然後渲染了一個子元件,我們通常把路由渲染成子元件,最後還處理了一個異常情況,提供了warning提示。

大部分時候是這樣用的。在react-router4中,也支援這種寫法,Provider也可以直接巢狀在自定義的react元件中。

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





在redux的配置檔案中,如果你使用了redux-logger,也許你會寫下面這樣一段程式碼:

    import thunk from 'redux-thunk';
    import promise from 'redux-promise';
    import createLogger from 'redux-logger';
    
    const logger = createLogger();
    const createStoreWithMiddleware = applyMiddleware(thunk, promise, logger)(createStore);
    const store = createStoreWithMiddleware(reducer);複製程式碼

現在,我們只關注redux-logger,我們可以看到使用redux-logger分為下面幾個步驟:

1、匯入redux-logger

import createLogger from 'redux-logger';複製程式碼

2、執行createLogger方法,將返回結果賦值給常量

const logger = createLogger();複製程式碼

3、將looger傳入applyMiddleware()

applyMiddleware(logger)複製程式碼

有2個難點,第一是createLogger()的返回值到底是如何實現的。第二就是applyMiddleware方法如何處理返回值。因為本文是講redux-logger的實現,所以我們只分析createLogger()

redux-logger中createLogger方法原始碼

const repeat = (str, times) => (new Array(times + 1)).join(str);
    const pad = (num, maxLength) => repeat(`0`, maxLength - num.toString().length) + num;
    
    //使用新的效能API可以獲得更好的精度(如果可用)
    const timer = typeof performance !== `undefined` && typeof performance.now === `function` ? performance : Date;
    
    function createLogger(options = {}) {
      return ({ getState }) => (next) => (action) => {
        const {
          level, //級別
          logger, //console的API
          collapsed, //
          predicate, //logger的條件
          duration = false, //列印每個action的持續時間
          timestamp = true, //列印每個action的時間戳
          transformer = state => state, //在列印之前轉換state
          actionTransformer = actn => actn, //在列印之前轉換action
        } = options;
    
        const console = logger || window.console;
    
        // 如果控制檯未定義則退出
        if (typeof console === `undefined`) {
          return next(action);
        }
    
        // 如果謂詞函式返回false,則退出
        if (typeof predicate === `function` && !predicate(getState, action)) {
          return next(action);
        }
    
        const started = timer.now();
        const prevState = transformer(getState());
    
        const returnValue = next(action);
        const took = timer.now() - started;
    
        const nextState = transformer(getState());
    
        // 格式化
        const time = new Date();
        const isCollapsed = (typeof collapsed === `function`) ? collapsed(getState, action) : collapsed;
    
        const formattedTime = timestamp ? ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}` : ``;
        const formattedDuration = duration ? ` in ${took.toFixed(2)} ms` : ``;
        const formattedAction = actionTransformer(action);
        const message = `action ${formattedAction.type}${formattedTime}${formattedDuration}`;
        const startMessage = isCollapsed ? console.groupCollapsed : console.group;
    
        // 渲染
        try {
          startMessage.call(console, message);
        } catch (e) {
          console.log(message);
        }
    
        if (level) {
          console[level](`%c prev state`, `color: #9E9E9E; font-weight: bold`, prevState);
          console[level](`%c action`, `color: #03A9F4; font-weight: bold`, formattedAction);
          console[level](`%c next state`, `color: #4CAF50; font-weight: bold`, nextState);
        } else {
          console.log(`%c prev state`, `color: #9E9E9E; font-weight: bold`, prevState);
          console.log(`%c action`, `color: #03A9F4; font-weight: bold`, formattedAction);
          console.log(`%c next state`, `color: #4CAF50; font-weight: bold`, nextState);
        }
    
        try {
          console.groupEnd();
        } catch (e) {
          console.log(`—— log end ——`);
        }
    
        return returnValue;
      };
    }
    
    export default createLogger;複製程式碼

解析redux-logger

1、入口函式createLogger(options = {}) 我們在redux配置檔案中呼叫的就是這個函式,也是redux-logger中唯一一個函式,它只有一個引數option,option是object。

2、return ({ getState }) => (next) => (action) => {} 這行程式碼看起來很複雜,一堆的箭頭函式,其實很簡單,createLogger()一定會有一個返回值,但是,我們在控制檯列印action資訊的時候,需要獲取state和action的資訊,所以,首先傳入getState方法,getState是redux提供的一個方法,用來獲取store的state。然後再傳入next方法,接著傳入action方法。next和action都是redux提供的方法,到這一步,我們就把需要的引數都傳入到函式中,可以進行下一步操作了。

3、定義option的配置引數 我們在使用redux-logger的時候,習慣了不配置任何引數,直接呼叫createLogger(),使用預設的配置。但其實還可以手動傳入一個option配置,不過並不常用。

const {
          level, //級別
          logger, //console的API
          collapsed, //
          predicate, //logger的條件
          duration = false, //列印每個action的持續時間
          timestamp = true, //列印每個action的時間戳
          transformer = state => state, //在列印之前轉換state
          actionTransformer = actn => actn, //在列印之前轉換action
        } = options;複製程式碼

4、定義console 如果你給option配置了console相關的API,那麼就使用你的配置,如果沒有配置,就使用window.console

const console = logger || window.console;複製程式碼

5、新增2個異常情況做退出處理 第一個if語句是控制檯未定義就返回下一個action操作,但是我想不到在瀏覽器中會出現console方法不存在的情況。 第二個if語句的predicate表示warn、log、error等屬於console的方法。&&表示2個條件要同時滿足才執行下面的操作。predicate(getState, action)其實就是類似console.log(getState, action)

// 如果控制檯未定義則退出
        if (typeof console === `undefined`) {
          return next(action);
        }
    
        // 如果謂詞函式返回false,則退出
        if (typeof predicate === `function` && !predicate(getState, action)) {
          return next(action);
        }複製程式碼

6、給各個常量賦值 為什麼會有這麼多常量呢?我們來看一張圖,圖上展示了需要列印的各種資訊。

react-redux的Provider和connect

總結出來就是:

action action.type @ timer
prev state {}
action {}
next state {}

這裡需要的是action.type, timer, 各種狀態下的state

const started = timer.now();
    const prevState = transformer(getState());
    
    const returnValue = next(action);
    const took = timer.now() - started;
    
    const nextState = transformer(getState());
    
    // 格式化
    const time = new Date();
    const isCollapsed = (typeof collapsed === `function`) ? collapsed(getState, action) : collapsed;
    
    const formattedTime = timestamp ? ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}` : ``;
    const formattedDuration = duration ? ` in ${took.toFixed(2)} ms` : ``;
    const formattedAction = actionTransformer(action);
    const message = `action ${formattedAction.type}${formattedTime}${formattedDuration}`;
    const startMessage = isCollapsed ? console.groupCollapsed : console.group;複製程式碼

上面程式碼資訊量比較大,我們還可以拆分出來看看。

a、先獲取一個開始時間started,然後讀取state,這個state是之前的狀態prevState。returnValue是返回值,返回下一個action。took是你執行完前面3行程式碼之後的真實時間,在這裡因為沒有用到非同步處理,所以我暫且認為transformer()和next()是同步的。nextState是新的state。

這段程式碼歸納起來看就是先讀取開始時間,然後讀取state,這個state因為還有更新action,所以是舊的state,然後執行next傳入新的action,更新完成之後,獲取結束時間,計算更新action的時間差,然後再獲取更新後的state。

    const started = timer.now();
    const prevState = transformer(getState());        
    const returnValue = next(action);
    const took = timer.now() - started;
    const nextState = transformer(getState());複製程式碼

b、下面的程式碼做了一件事情,設定列印的資訊。

formattedTime是列印出來的時間,格式是 時:分:秒,formattedDuration是時間差,formattedAction是當前的action方法。isCollapsed用處不大,不管他。

// 格式化
    const time = new Date();
    const isCollapsed = (typeof collapsed === `function`) ? collapsed(getState, action) : collapsed;
    
    const formattedTime = timestamp ? ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}` : ``;
    const formattedDuration = duration ? ` in ${took.toFixed(2)} ms` : ``;
    const formattedAction = actionTransformer(action);
    const message = `action ${formattedAction.type}${formattedTime}${formattedDuration}`;
    const startMessage = isCollapsed ? console.groupCollapsed : console.group;複製程式碼

這幾行程式碼做的事情也非常簡單,給需要列印的常量賦值。然後組合之後賦值給message:

const message = `action ${formattedAction.type}${formattedTime}${formattedDuration}`;複製程式碼

message == action action.type @ time

7、try {} catch() {} 部分一般不會用到,也可以不管。

startMessage.call(console, message);表示將message當做引數傳入startMessage,call的第一個引數是指執行環境,意思就是在console列印message資訊。

try {
      startMessage.call(console, message);
    } catch (e) {
      console.log(message);
    }複製程式碼

8、列印console的資訊,這就圖上列印出來的部分了。

因為我們通常沒有配置level,所以執行的是else語句的操作。

    if (level) {
          console[level](`%c prev state`, `color: #9E9E9E; font-weight: bold`, prevState);
          console[level](`%c action`, `color: #03A9F4; font-weight: bold`, formattedAction);
          console[level](`%c next state`, `color: #4CAF50; font-weight: bold`, nextState);
        } else {
          console.log(`%c prev state`, `color: #9E9E9E; font-weight: bold`, prevState);
          console.log(`%c action`, `color: #03A9F4; font-weight: bold`, formattedAction);
          console.log(`%c next state`, `color: #4CAF50; font-weight: bold`, nextState);
        }複製程式碼

9、遊戲結束

try {
          console.groupEnd();
        } catch (e) {
          console.log(`—— log end ——`);
        }複製程式碼

10、返回值

return returnValue;複製程式碼

總結

redux-logger做的事情是在控制檯輸出action的資訊,所以首先要獲取前一個action,當前action,然後是下一個action。看完之後,你對redux-logger原始碼的理解加深了嗎?





在react開發中,一部分人使用redux-thunk,一部分人使用redux-saga,彼此各有優點。

今天我們來研究一下redux-thunk的原始碼,看看它到底做了什麼事情。

使用場景

import { createStore, applyMiddleware } from 'redux';
    import thunk from 'redux-thunk';
    import rootReducer from './reducers/index';
    //註冊thunk到applyMiddleware
    const createStoreWithMiddleware = applyMiddleware(
      thunk
    )(createStore);
    
    const store = createStoreWithMiddleware(rootReducer);
    
    //action方法
    function increment() {
      return {
        type: INCREMENT_COUNTER
      };
    }
    //執行一個非同步的dispatch
    function incrementAsync() {
      return dispatch => {
        setTimeout(() => {
          dispatch(increment());
        }, 1000);
      };
    }複製程式碼

主要程式碼:

1、匯入thunk

import thunk from 'redux-thunk';複製程式碼

2、新增到applyMiddleware()

const createStoreWithMiddleware = applyMiddleware(
      thunk
    )(createStore);複製程式碼

我們可以猜測thunk是一個object。

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

一共11行,簡潔,超簡潔,5K+ star。

原始碼分析

1、定義了createThunkMiddleware()方法,可以傳入引數extraArgument。

function createThunkMiddleware(extraArgument){}複製程式碼

2、該方法返回的是一個action物件。

我們知道action本身是一個object,帶有type和arguments。我們將dispatch和getState傳入action,next()和action()是redux提供的方法。接著做判斷,如果action是一個function,就返回action(dispatch, getState, extraArgument),否則返回next(action)。

return ({ dispatch, getState }) => next => action => {
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }
    
        return next(action);
      };複製程式碼

3、執行createThunkMiddleware()

這一步的常量thunk是一個物件,類似{type: "", arg1, arg2, ...}

const thunk = createThunkMiddleware();複製程式碼

4、給thunk設定一個變數withExtraArgument,並且將createThunkMiddleware整個函式賦給它。

thunk.withExtraArgument = createThunkMiddleware;複製程式碼

5、最後匯出thunk。

export default thunk;複製程式碼

總結

什麼是thunk?thunk是一箇中間函式,它的返回值是一個表示式。action裡面可能傳遞多個引數,我們不可能再專門替每個action寫一個傳遞方法。那麼就有了thunk的出現,thunk可以將多個引數的函式作為一個引數傳遞。

例如有這樣一個action,帶有多個引數:

function test(arg1, arg2, ...) {
        return {
            type: "TEST",
            arg1,
            arg2,
            ...
        }
    }複製程式碼

然後我們執行dispatch()方法,我們需要把test()函式作為一個引數傳遞。這樣就解決了多引數傳遞的問題,這個test()就成了一個thunk。

如果你對redux-thunk還有疑問,可以檢視這個解釋:redux-thunk of stackoverflow


相關文章