1. 程式人生 > 程式設計 >詳解JavaScript狀態容器Redux

詳解JavaScript狀態容器Redux

目錄
  • 一、Why Redux
  • 二、Redux Data flow
  • 三、Three Principles(三大原則)
  • 四、Redux原始碼解析
    • 4.1、index.js
    • 4.2、createStore.js
    • 4.3、combineReducers.js
    • 4.4、bindActionCreators.js
    • 4.5、compose.js
    • 4.6、applyMiddleware.js
  • 五、從零開始實現一個簡單的Redux
    • 六、Redux Devtools
      • 七、總結

        一、Why Redux

        在說為什麼用 Redux 之前,讓我們先聊聊元件通訊有哪些方式。常見的元件通訊方式有以下幾種:

        • 父子元件:props、state/callback回撥來進行通訊
        • 單頁面應用:路由傳值
        • 全域性事件比如EventEmitter監聽回撥傳值
        • react中跨層級元件資料傳遞Context(上下文)

        在小型、不太複雜的應用中,一般用以上幾種元件通訊方式基本就足夠了。

        但隨著應用逐漸複雜,資料狀態過多(比如服務端響應資料、瀏覽器快取資料、UI狀態值等)以及狀態可能會經常發生變化的情況下,使用以上元件通訊方式會很複雜、繁瑣以及很難定位、除錯相關問題。

        因此狀態管理框架(如 vuex、MobX、Redux等)就顯得十分必要了,而 Redux 就是其中使用最廣、生態最http://www.cppcns.com完善的。

        二、Redux Data flow

        在一個使用了 Redux 的 App應用裡面會遵循下面四步:

        第一步:通過store.dispatch(action)來觸發一個action,action就是一個描述將要發生什麼的物件。如下:

        { type: 'LIKE_ARTICLE',articleId: 42 }
        { type: 'FETCH_USER_SUCCESS',response: { id: 3,name: 'Mary' } }
        { type: 'ADD_TODO',text: '金融前端.' }

        第二步:Redux會呼叫你提供的 Reducer函式。

        第三步:根 Reducer 會將多個不同的 Reducer 函式合併到單獨的狀態樹中。

        第四步:Redux store會儲存從根 Reducer 函式返回的完整狀態樹。

        所謂一圖勝千言,下面我們結合 Redux 的資料流圖來熟悉這一過程。

        詳解JavaScript狀態容器Redux

        三、Three Principles(三大原則)

        1、Single source of truth:單一資料來源,整個應用的state被儲存在一個物件樹中,並且只存在於唯一一個store中。

        2、State is read-only:state裡面的狀態是隻讀的,不能直接去修改state,只能通過觸發action來返回一個新的state。

        3、Changes are made with pure functions:要使用純函式來修改state。

        四、Redux原始碼解析

        Redux 原始碼目前有js和ts版本,本文先介紹 js 版本的 Redux 原始碼。Redux 原始碼行數不多,所以對於想提高原始碼閱讀能力的開發者來說,很值得前期來學習。

        Redux原始碼主要分為6個核心js檔案和3個工具js檔案,核心js檔案分別為index.js、createStore.js、compose.js、combineRuducers.js、bindActionCreators.js和applyMiddleware.js檔案。

        接下來我們來一一學習。

        4.1、index.js

        index.js是入口檔案,提供核心的API,如createStore、combineReducers、applyMiddleware等。

        export {
          createStore,combineReducers,bindActionCreators,applyMiddleware,compose,__DO_NOT_USE__ActionTypes
        }

        4.2、createStore.js

        createStore是 Redux 提供的API,用來生成唯一的store。store提供getState、dispatch、subscibe等方法,Redux 中的store只能通過dispatch一個action,通過action來找對應的 Reducer函式來改變。

        export default function createStore(reducer,preloadedState,enhancer) {
        ...
        }

        從原始碼中可以知道,createStore接收三個引數:Reducer、preloadedState、enhancer。

        Reducer是action對應的一個可以修改store中state的純函式。

        preloadedState代表之前state的初始化狀態。

        enhancer是中介軟體通過applyMiddleware生成的一個加強函式。store中的getState方法是獲取當前應用中store中的狀態樹。

        /**
         * Reads the state tree managed by the store.
         *
         * @returns {any} The current state tree of your application.
         */
        function getState() {
          if (isDispatching) {
            throw new Error(
              'You may not call store.getState() while the reducer is executing. ' +
                'The reducer has already received the state as an argument. ' +
                'Pass it down from the top reducer instead of reading it from the store.'
            )
          }
          return currentState
        }

        dispatch方法是用來分發一個action的,這是唯一的一種能觸發狀態發生改變的方法。subscribe是一個監聽器,當一個action被dispatch的時候或者某個狀態發生改變的時候會被呼叫。

        4.3、combineReducers.js

        /**
         * Turns an object whose values are different reducer functions,into a single
         * reducer function. It will call every child reducer,and gather their results
         * into a single state object,whose keys correspond to the keys of the passed
         * reducer functions.
         */
        export default function combineReducers(reducers) {
          const reducerKeys = Object.keys(reducers)
             ...
          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
              //判斷state是否發生改變
              hasChanged = hasChanged || nextStateForKey !== previousStateForKey
            }
            //根據是否發生改變,來決定返回新的state還是老的state
            return hasChanged ? nextState : state
          }
        }

        從原始碼可以知道,入參是 Reducers,返回一個function。combineReducers就是將所有的 Reducer合併成一個大的 Reducer 函式。核心關鍵的地方就是每次 Reducer 返回新的state的時候會和老的state進行對比,如果發生改變,則hasChanged為true,觸發頁面更新。反之,則不做處理。

        4.4、bindActionCreators.js

        /**
         * Turns an object whose values are action creators,into an object with the
         * same keys,but with every function wrapped into a `dispatch` call so they
         * may be invoked directly. This is just a convenience method,as you can call
         * `store.dispatch(MyActionCreators.doSomething())` yourself just fine.
         */
        function bindActionCreator(actionCreator,dispatch) {
          return function() {
            return dispatch(actionCreator.apply(this,arguments))
          }
        }
         
        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)
            }
          }
          return boundActionCreators
        }

        bindActionCreator是將單個actionCreator繫結到dispatch上,bindActionCreators就是將多個actionCreators繫結到dispatch上。

        bindActionCreator就是將傳送actions的過程簡化,當呼叫這個返回的函式時就自動呼叫dispatch,傳送對應的action。

        bindActionCreators根據不同型別的actionCreators做不同的處理,actionCreators是函式就返回函式,是物件就返回一個物件。主要是將actions轉化為dispatch(action)格式,方便進行actions的分離,並且使程式碼更加簡潔。

        4.5、compose.js

        /**
         * Composes single-argument functions from right to left. The rightmost
         * function can take multiple arguments as it provides the signature for
         * the resulting composite function.
         *
         * @param {...Function} funcs The functions to compose.
         * @returns {Function} A function obtained by composing the argument functions
         * from right to left. For example,compose(f,g,h) is identical to doing
         * (...args) => f(g(h(...args))).
         */
         
        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)))
        }

        compose是程式設計客棧函式式變成裡面非常重要的一個概念,在介紹compose之前,先來認識下什麼是 Reduce?官方文件這麼定義reduce:reduce()方法對累加器和陣列中的每個元素(從左到右)應用到一個函式,簡化為某個值。compose是柯里化函式,藉助於Reduce來實現,將多個函式合併到一個函式返回,主要是在middleware中被使用。

        4.6、applyMiddleware.js

        /**
         * Creates a store enhancer that applies middleware to the dispatch method
         * of the Redux store. This is handy for a variety of tasks,such as expressing
         * asynchronous actions in a concise manner,or logging every action payload.
         */
        export default function applyMiddleware(...middlewares) {
          return createStore => (...args) => {
            const store = createStore(...args)
            ...
            ...
            return {
              ...store,dispatch
            }
          }
        }

        applyMiddleware.js檔案提供了middleware中介軟體重要的API,middleware中介軟體主要用來對store.dispatch進行重寫,來完善和擴充套件dispatch功能。

        那為什麼需要中介軟體呢?

        首先得從Reducer說起,之前 Redux三大原則裡面提到了reducer必須是純函式,下面給出純函式的定義:

        • 對於同一引數,返回同一結果
        • 結果完全取決於傳入的引數
        • 不產生任何副作用

        至於為什麼reducer必須是純函式,可以從以下幾點說起?

        • 因為 Redux 是一個可預測的狀態管理器,純函式更便於 Redux進行除錯,能更方便的跟蹤定位到問題,提高開發效率。
        • Redux 只通過比較新舊物件的地址來比較兩個物件是否相同,也就是通過淺比較。如果在 Reducer 內部直接修改舊的state的屬性值,新舊兩個物件都指向同一個物件,如果還是通過淺比較,則會導致 Redux 認為沒有發生改變。但要是通過深比較,會十分耗費效能。最佳的辦法是 Redux返回一個新物件,新舊物件通過淺比較,這也是 Reducer是純函式的重要原因。

        Reducer是純函式,但是在應用中還是會需要處理記錄日誌/異常、以及非同步處理等操作,那該如何解決這些問題呢?

        這個問題的答案就是中介軟體。可以通過中介軟體增強dispatch的功能,示例(記錄日誌和異常)如下:

        const store = createStore(reducer);
        const next = store.dispatch;
         
        // 重寫store.dispatch
        store.dispatch = (action) => {
            try {
                console.log('action:',action);
                console.log('current state:',store.getState());
                next(action);
                console.log('next state',store.getState());
            } catch (error){
                console.error('msg:',error);
            }
        }

        五、從零開始實現一個簡單的Redux

        既然是要從零開始實現一個Redux(簡易計數器),那麼在此之前我們先忘記之前提到的store、Reducer、dispatch等各種概念,只需牢記Redux是一個狀態管理器。

        首先我們來看下面的程式碼:

        let state = {
            count : 1
        }
        //修改之前
        console.log (state.count);
        //修改count的值為2
        state.count = 2;
        //修改之後
        cunQpIgVfFonsole.log (state.count);

        我們定義了一個有count欄位的state物件,同時能輸出修改之前和修改之後的count值。但此時我們會發現一個問題?就是其它如果引用了count的地方是不知道count已經發生修改的,因此我們需要通過訂閱-釋出模式來監聽,並通知到其它引用到count的地方。因此我們進一步優化程式碼如下:

        let state = {
            count: 1
        };
        //訂閱
        function subscribe (listener) {
            listeners.push(listener);
        }
        function changeState(count) {
            state.count = count;
            for (let i = 0; i < listeners.length; i++) {
                const listener = listeners[i];
                listener();//監聽
            }
        }

        此時我們對count進行修改,所有的listeners都會收到通知,並且能做出相應的處理。但是目前還會存在其它問題?比如說目前state只含有一個count欄位,如果要是有多個欄位是否處理方式一致。同時還需要考慮到公共程式碼需要進一步封裝,接下來我們再進一步優化:

        const createStore = function (initState) {
            let state = initState;
            //訂閱
            function subscribe (listener) {
                listeners.push(listener);
            }
            function changeState (count) {
         http://www.cppcns.com       state.count = count;
                for (let i = 0; i < listeners.length; i++) {
                    const listener = listeners[i];
                    listener();//通知
                }
            }
            function getState () {
                return state;
            }
            return {
                subscribe,changeState,getState
            }
        }

        我們可以從程式碼看出,最終我們提供了三個API,是不是與之前Redux原始碼中的核心入口檔案index.js比較類似。但是到這裡還沒有實現Redux,我們需要支援新增多個欄位到state裡面,並且要實現Redux計數器。

        let initState = {
            counter: {
                count : 0
            },info: {
                name: '',description: ''
            }
        }
        let store = createStore(initState);
        //輸出count
        store.subscribe(()=>{
            let state = store.getState();
            console.log(state.counter.count);
        });
        //輸出info
        store.subscribe(()=>{
            let state = store.getState();
            console.log(`${state.info.name}:${state.info.description}`);
        });

        通過測試,我們發現目前已經支援了state裡面存多個屬性欄位,接下來我們把之前changeState改造一下,讓它能支援自增和自減。

        //自增
        store.changeState({
            count: store.getState().count + 1
        });
        //自減
        store.changeState({
            count: store.getState().count - 1
        });
        //隨便改成什麼
        store.changeState({
            count: 金融
        });

        我們發現可以通過changeState自增、自減或者隨便改,但這其實不是我們所需要的。我們需要對修改count做約束,因為我們在實現一個計數器,肯定是隻希望能進行加減操作的。所以我們接下來對changeState做約束,約定一個plan方法,根據type來做不同的處理。

        function plan (state,action) => {
          switch (action.type) {
            case 'INCREMENT':
              return {
                ...state,count: state.count + 1
              }
            case 'DECREMENT':
              程式設計客棧return {
                ...state,count: state.count - 1
              }
            default:
              return state
          }
        }
        let store = createStore(plan,initState);
        //自增
        store.changeState({
            type: 'INCREMENT'
        });
        //自減
        store.changeState({
            type: 'DECREMENT'
        });

        我們在程式碼中已經對不同type做了不同處理,這個時候我們發現再也不能隨便對state中的count進行修改了,我們已經成功對changeState做了約束。我們把plan方法做為createStore的入參,在修改state的時候按照plan方法來執行。到這裡,恭喜大家,我們已經用Redux實現了一個簡單計數器了。

        這就實現了 Redux?這怎麼和原始碼不一樣啊

        然後我們再把plan換成reducer,把changeState換成dispatch就會發現,這就是Redux原始碼所實現的基礎功能,現在再回過頭看Redux的資料流圖是不是更加清晰了。

        詳解JavaScript狀態容器Redux

        六、Redux Devtools

        Redux devtools是Redux的除錯工具,可以在Chrome上安裝對應的外掛。對於接入了Redux的應用,通過 Redux devtools可以很方便看到每次請求之後所發生的改變,方便開發同學知道每次操作後的前因後果,大大提升開發除錯效率。

        詳解JavaScript狀態容器Redux

        如上圖所示就是 Redux devtools的視覺化介面,左邊操作介面就是當前頁面渲染過程中執行的action,右側操作介面是State儲存的資料,從State切換到action面板,可以檢視action對應的 Reducer引數。切換到Diff面板,可以檢視前後兩次操作發生變化的屬性值。

        七、總結

        Redux 是一款優秀的狀態管理器,原始碼短小精悍,社群生態也十分成熟。如常用的react-redux、dva都是對 Redux 的封裝,目前在大型應用中被廣泛使用。這裡推薦通過Redux官網以及原始碼來學習它核心的思想,進而提升閱讀原始碼的能力。

        以上就是詳解javascript狀態容器Redux的詳細內容,更多關於javaScript狀態容器Redux的資料請關注我們其它相關文章!