redux原理分享

空山與新雨發表於2022-11-27

概述

  • 一個狀態管理工具
  • Store:儲存資料的地方,你可以把它看成一個容器,整個應用只能有一個 Store。
  • State:包含所有資料,如果想得到某個時點的資料,就要對 Store 生成快照,這種時點的資料集合,就叫做 State。
  • Action:Action 就是 View 發出的通知,表示 State 應該要發生變化了。
  • Action Creator:View 要傳送多少種訊息,就會有多少種 Action。如果都手寫,會很麻煩,所以我們定義一個函式來生成 Action,這個函式就叫 Action Creator。
  • Reducer:Store 收到 Action 以後,必須給出一個新的 State。這種 State 的計算過程就叫做 Reducer。Reducer 是一個函式,它接受 Action 和當前 State 作為引數,返回一個新的 State。
  • dispatch:是 View 發出 Action 的唯一方法。

整個工作流程:

  1. 首先,使用者(透過 View)發出 Action,發出方式就用到了 dispatch 方法。
  2. 然後,Store 自動呼叫 Reducer,並且傳入兩個引數:當前 State 和收到的 Action,Reducer 會返回新的 State
  3. State 一旦有變化,Store 就會呼叫監聽函式,來更新 View。

三大原則

1.單一資料來源(Store) 整個應用的State被存放在一棵Object tree中,並且這個Object tree只存在唯一一個Store中;

2.State是隻讀的 唯一改變 State 的方法就是觸發 Action,Action 是一個用於描述已發生事件的普通物件。 確保了所有的修改都能被集中化處理。

3.透過純函式Reducer來修改Store, Reducer 只是一些純函式,它接收先前的 State 和 Action,並返回新的 State。 即reducer(state, action) => new state

createStore建立store

  • createStore 方法接受 3 個引數引數 (reducer, [preloadedState], enhancer);
    返回 store,store 上掛載著 dispatch、getState、subscribe、replaceReducer 等方法
  • 第二個引數是 preloadedState,它是 state 的初始值,實際上他並不僅僅是扮演著一個 initialState 的角色,如果我們第二個引數是函式型別,createStore 會認為傳入了一個 enhancer,如果我們傳入了一個 enhancer,createStore 會返回 enhancer(createStore)(reducer, preloadedState)的呼叫結果,這是常見高階函式的呼叫方式。
  • enhancer 接受 createStore 作為引數,對 createStore 的能力進行增強,並返回增強後的 createStore。然後再將 reducer 和 preloadedState 作為引數傳給增強後的 createStore,最終得到生成的 store。
export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    // 第二個引數是一個函式,沒有第三個引數的情況
    enhancer = preloadedState;
    preloadedState = undefined;
  }
  // 如果第三個引數是函式走下面的邏輯,返回一個新的createStore
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      // enhancer 不是函式就報錯
      throw new Error('Expected the enhancer to be a function.');
    }
    // enhancer就是高階函式,強化了本身這個createStore的函式,拿到增強後的createStore函式去處理
    // applyMiddleware這個函式還會涉及到這個
    return enhancer(createStore)(reducer, preloadedState);
  }
  if (typeof reducer !== 'function') {
    // reducer不是函式報錯
    throw new Error('Expected the reducer to be a function.');
  }
  // 其他程式碼省略
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable,
  };
}

applyMiddleware 應用中介軟體

  • 返回一個函式 enhancer;
  • 右邊中介軟體的執行時機由左邊的中介軟體決定,這是因為 next 的方法的呼叫時機由右邊控制
  • dispatch 的處理使用了閉包,這樣保證在中介軟體中呼叫 dispatch 也是用的最終的 dispatch,也是同一個 dispatch;
  • 寫中介軟體的時候如果要呼叫 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,
      };
    };
}
// 其實就是修改了dispatch
let store = applyMiddleware(middleware1,middleware2)(createStore)(rootReducer);

combineReducers 合併多個reducer

從執行結果看,這時候 state 已經變成了一個以這些 reducer 為 key 的物件;reducer 也變成了一個合併的 reducer;
遍歷執行所有的 reducer 的時候把 action 傳進去,返回新的 state;

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);
  /* 返回一個整合後的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;
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    return hasChanged ? nextState : state;
  };
}

dispatch

  • 預設的 action 只能是普通物件,除非使用了第三方中介軟體,比如 redux-thunk
function dispatch(action) {
  if (!isPlainObject(action)) {
    throw new Error(
      'Actions must be plain objects. ' +
        'Use custom middleware for async actions.',
    );
  }

  if (typeof action.type === 'undefined') {
    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;
    currentState = currentReducer(currentState, action);
  } finally {
    isDispatching = false;
  }

  var listeners = (currentListeners = nextListeners);

  for (var i = 0; i < listeners.length; i++) {
    var listener = listeners[i];
    listener();
  }
  return action;
}

中介軟體

  • Redux 中介軟體在發起一個 action 至 action 到達 reducer 的之間提供了一個第三方的擴充套件。本質上透過外掛的形式,將原本的 action->redux 的流程改變為 action->middleware1->middleware2-> ... ->reducer,透過改變資料流,從而實現例如非同步 Action、日誌輸入的功能。
  • Redux 中介軟體正規化
    • 一箇中介軟體接收 store 作為引數,會返回一個函式
    • 返回的這個函式接收老的 dispatch 函式作為引數(一般用 next 作為形參),會返回一個新的函式
    • 返回的新函式就是新的 dispatch 函式,這個函式里面可以拿到外面兩層傳進來的 store 和老 dispatch 函式
function logger(store) {
  return function (next) {
    return function (action) { // 新的 dispatch 函式
      console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result;
    };
  };
}

中介軟體的呼叫順序

  • 首先呼叫順序和書寫順序是一致的,但是這裡面的洋蔥模型包含了兩次順序,從洋蔥出來的順序是和書寫順序相反
import React from 'react';
import { createStore, applyMiddleware } from 'redux';
function createLogger({ getState, dispatch }) {
  return (next) => (action) => {
    const prevState = getState();
    console.log('createLogger1');
    const returnValue = next(action);
    const nextState = getState();
    const actionType = String(action.type);
    const message = `action ${actionType}`;

    console.log(`%c prev state`, `color: #9E9E9E`, prevState);
    console.log(`%c action`, `color: #03A9F4`, action);
    console.log(`%c next state`, `color: #4CAF50`, nextState);
    return returnValue;
  };
}

function createLogger2({ getState }) {
  return (next) => (action) => {
    const console = window.console;
    const prevState = getState();
    console.log('createLogger2');
    const returnValue = next(action);
    const nextState = getState();
    const actionType = String(action.type);
    const message = `action ${actionType}`;

    console.log(`%c prev state2`, `color: #9E9E9E`, prevState);
    console.log(`%c action2`, `color: #03A9F4`, action);
    console.log(`%c next state2`, `color: #4CAF50`, nextState);
    return returnValue;
  };
}
const reducer = function (state = { number: 0 }, action) {
  switch (action.type) {
    case 'add':
      return {
        number: state.number + action.number,
      };
    default:
      return state;
  }
};

const store = createStore(
  reducer,
  applyMiddleware(createLogger, createLogger2),
);
store.subscribe(function () {
  console.log(1111);
});
const { dispatch } = store;
const App = () => {
  const handler = () => {
    dispatch({ type: 'add', number: 10 });
  };
  return (
    <div>
      <button onClick={handler}>觸發redux</button>
    </div>
  );
};
export default App;

store

store 的屬性如下:

  1. dispatch: ƒ dispatch(action)
  2. getState: ƒ getState()
  3. replaceReducer: ƒ replaceReducer(nextReducer)
  4. subscribe: ƒ subscribe(listener)

redux 資料流

Redux 的資料流是這樣的:
介面 => action => reducer => store => react => virtual dom => 介面

bindActionCreators

將action物件轉為一個帶dispatch的方法

比如connect接收的mapDispatchToProps 是物件,會使用 bindActionCreators 處理; 接收 actionCreator 和 dispatch,返回一個函式;

function bindActionCreator(actionCreator, dispatch) {
  // 返回一個函式
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}
function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }
  const boundActionCreators = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

const mapDispatchToProps = {  // actionCreators 這是個集合,
  onClick: (filter) => {
    type: 'SET_VISIBILITY_FILTER',
      filter: filter
  };
}

轉換為:
const mapDispatchToProps = {  // actionCreators 這是個集合,
  onClick:function(filter) {
    return dispatch({  // dispatch 是閉包中的方法
      type: 'SET_VISIBILITY_FILTER',
      filter: filter
    })
  }
}

compose

函式套函式,compose(...chain)(store.dispatch)結果返回一個加強了的 dispatch;

這點和koa比較相似,這個 dispatch 在執行的時候會呼叫中介軟體。

function compose(...funcs) {
  if (funcs.length === 0) {
    return (arg) => arg;
  }

  if (funcs.length === 1) {
    return funcs[0];
  }
  // 每一次reduce迭代都會返回一個加強版的dispatch
  return funcs.reduce(
    (a, b) =>
      (...args) =>
        a(b(...args)),
  );
}

加強版 dispatch(一個方法,接收 action 引數),在中介軟體中用 next 表示,執行 next 之後,會形成一個鏈條。

enhancer

  • enhancer,翻譯過來就是 store 加強器,比如 applymiddleware 的返回值就是一個 enhaner。store 加強器可以重新構建一個更強大的 store,來替換之前的基礎版的 store,讓你的程式可以增加很多別的功能,比如 appllymiddleware 可以給你的 redux 增加中介軟體,使之可以擁有非同步功能,日誌功能等!
// 以createStore為引數
(createStore) =>
  (...args) => {};

使用 redux 常遇見的問題

  • 樣板程式碼過多 增加一個 action 往往需要同時定義相應的 actionType 然後再寫相關的 reducer。例如當新增一個非同步載入事件時,需要同時定義載入中、載入失敗以及載入完成三個 actionType,需要一個相對應的 reducer 透過 switch 分支來處理對應的 actionType,冗餘程式碼過多;

    目前已經存在著非常多的解決方案,比如dva redux-tookit等。

  • 更新效率問題:由於使用不可變資料模式,每次更新 state 都需要複製一份完整的 state 造成了記憶體的浪費以及效能的損耗。

    其實 redux 以及 react-redux 中內部已做最佳化,開發的時候使用 shouldComponentUpdate 等最佳化方法也可以應用,也可以用不可變資料結構如 immutable、Immr 等來解決複製開銷問題。

相關文章