教你如何實現一個簡易的 redux.js 和 redux 中間件

首先咱們要弄清楚 reduxjs 的思想、做用是什麼,這樣咱們才能開始下一步的構思。在我看來 reduxjs 核心就是一種單一數據源的概念,數據存儲在一個函數的 state 變量中,只能經過固定的方法去修改和獲取 dispatch()、getState()react

在 SPA 應用中,reduxjs 被普遍使用。對數據進行統一管理、實現數據共享,一般組件和組件之間、頁面和頁面之間能夠數據共享。在 react 開發中,我常常將共用的數據和異步請求數據存放在 state 中。經過 props 的形式存在,只要在一個組件中對數據源進行了修改,其餘共享的組件都會及時獲得更新和渲染UI界面。redux

如今咱們知道了關於 redux 的關鍵思想和用途,接下來咱們一步一步實現它。我會按照下面這個列表的順序給你們詳細說明:數組

  • createStore
  • reduce、combineReducers
  • applyMiddleware
  • 中間件原理
  • 改造後的 dispatch
  • redux 應用 demo

createStore()

function createStore(reducer, initState) {
    // 聲明一個初始化用的 action
    const INIT_ACTION = undefined;
    // 綁定監聽事件的集合
    const listeners = [];
    // 這就是咱們一直說的那個【數據源】
    // 參數 initState 能夠有,也能夠沒有。通常狀況下不須要傳遞
    let state = initState ? initState : {};
    
    function dispatch(action) {
      // action 必須是一個純對象,不能是其餘的類型
      if (Object.prototype.toString.call(action) === '[object Object]') {
        throw new Error('Actions must be plain objects');
      }

      // 注意:這裏是最終仍是經過調用 reducer 方法
      state = reducer(state, action);
      // 遍歷 listeners
      for (let i = 0; i < listeners.length; i++) {
        listeners[i]();
      }
    }

    // 獲取 state 數據
    function getState() {
      return state;
    }
    
    // 綁定監聽事件
    function subscription(listener) {
      listeners.push(listener);
      // 取消監聽,將事件從 listeners 中移除
      return function() {
        const idx = listeners.indexOf(listener);
        if (idx >= 0) {
          listeners.splice(idx, 1);
        }
      }
    }

    // 這是啥意思了,其實這是在調用 createStore() 時,就初始化了一個 state
    dispatch(INIT_ACTION);

    // 經過對象,將這些內部函數傳遞到外部。不要懷疑,這就是一個典型的閉包
    return {
      dispatch,
      getState,
      subscription,
    };
  }
複製代碼

createStore 方法中咱們能夠看出來,其實他就是 js模塊。利用了局部變量和閉包的特性,將 state 隱藏起來,只能經過閉包的形式進行訪問和修改。閉包

reduce、combineReducers

首先 reduce 它是一個函數,咱們能夠本身定義。咱們能夠把咱們的項目想像成以下的一個場景,修改用戶的信息:app

function userName(state = {}, action = {}) {
    switch (action.type) {
      case 'name':
        return { ...state, name: action.data };
      case 'age':
        return { ...state, age: action.data };
      case 'sex':
        return { ...state, sex: action.data };
      // 必須設置 default,直接返回 state
      default:
        return state;
    }
  }
複製代碼

若是咱們的項目中只須要這一種交互場景,那麼定義 userName() 就夠了。這個時候 咱們把 userName 傳遞給 createStore異步

const { getState } = createStore(userName);
  // 返回的是一個 {}
  console.log(getState());
複製代碼

上面的代碼在執行 createStore(userName) 時,內部執行一次 dispatch(INIT_ACTION) ,從而在 dispatch 方法內部調用了 userName({}, undefined)。因此打印的結果是一個空對象。函數

若是交互場景比較多的時候呢,一個 reducer 確定不夠用啊,那麼這個時候咱們可能會定義多個相似 userName 這個的 reducer 函數,因此咱們還須要定義一個工具函數 combineReducers,將多個 reducer 函數組合成一個 reducer 函數。工具

function combineReducers(reducers) {
    const keys = Object.keys(reducers);
    const finallyKeys = [];
    for (let i = 0; i < keys.length; i++) {
      if (typeof reducers[keys[i]] !== 'function') throw Error('reducer must be a function');
      finallyKeys.push(keys[i]);
    }

    // 看,最後返回的仍是一個 function
    return function(state = {}, action) {
      let hasChange = false;
      const newState = {};
      // 遍歷全部的 reducer 函數
      finallyKeys.forEach(key => {
        // 獲取這個 reducer 函數對應的 state。注意它多是一個 undefined
        // 沒錯,在 createStore() 中執行 dispatch(INIT_ACTION),這個時候 prevState_key 可能就是一個 unudefined
        const prevState_key = state[key];
        const reducer = reducers[key];
        // 調用該 reducer,返回一個新的 state
        const nextState_key = reducer(prevState_key, action);

        // 注意這裏,若是 reducer 函數返回的是一個 undefined。那麼這裏就會報錯了
        // 因此咱們在定義 reducer 函數時,應該有一個限制:若是沒有匹配到 action 的 type 。應該默認返回 previous state。
        if (typeOf nextState_key === 'undefined') {
          throw Error('to ignore an action, you must explicitly return the previous state');
        }

        // 當 reducer 執行完成時,會在 newState 上添加一個新屬性,屬性值就是 nextState_key
        // 其實,從這個地方咱們就應該能夠猜想到,最終獲得的 state【數據源】,它的結果應該和咱們傳入的 reducers 結構是同樣的
        newState[key] = nextState_key;
        hasChange = hasChange || nextState_key !== prevState_key;
      });
      return hasChange ? newState : state;
    }
  }
複製代碼

結合以前的 createStore,咱們看看下面的 demo:ui

function menu(state = {}, action = {}) {
    switch (action.type) {
      case 'home':
        return { ...state, home: action.data };
      case 'list':
        return { ...state, list: action.data };
      case 'detail':
        return { ...state, detail: action.data };
      default:
        return state;
    }
  }

  const reducer = combineReducers({ userName, menu });
  const { getState } = createStore(userName);
  // 返回的是一個 { userName: {}, menu: {} }
  // 這裏和咱們傳遞給 combineReducers() 中的參數的結構是一致的。
  console.log(getState());
複製代碼

上面的 reduceruserName, menu 的一個組合體,因此每次調用 dispatch(action) 時,都會遍歷全部的 reducers。還有一個很重要的地方就是,每一個 reducer 函數在沒有匹配到 action.type 時,必須把 reducer() 的參數 state 做爲返回值,不然就報錯。spa

applyMiddleware

reduxjs 還有一個很是厲害的功能,就是能夠利用中間件,作不少事情。好比說,咱們比較經常使用的 redux-thunk、redux-logger 等。

// 這裏先不考慮參數爲空的狀況
  function compose() {
    const middleware = [...arguments];
    // 這裏利用了redux 高階函數 
    // 第一次執行時,將 middleware 中的第一個和第二個元素賦值給 a、b。而後將返回的結果函數 fn 賦值給 a。
    // 第二次執行時,a 就是上一次的執行結果,這個時候將 middleware 中的第三個元素賦值給 b。而後將返回的結果函數 fn 賦值給 a。
    // 第三次,第四次。依次類推。。。
    return middleware.reduce(function(a, b) {
      return function fn () {
        return a(b.apply(null, arguments));
      }
    });
  } 

  function applyMiddlyWare(createStore) {
    return function(reducer) {
      // 接收中間件做爲參數
      return function(...middlewares) {
        const { dispatch, getState, subscription } = createStore(reducer);
        // 將 dispatch 賦值給變量 _dispatch
        let _dispatch = dispatch;

        const disp = (...args) => {
          _dispatch(...args);
        }

        // 將上面定義 disp 內部函數,傳遞給每個中間件函數
        // 因此上面的 disp 就構成了一個閉包
        const chain = middlewares.map(middleware => middleware({ dispatch: disp, getState }));

        // 這裏又對變量 _dispatch 進行了賦值。這裏理解可能有點繞,後面再詳細介紹
        // 注意這裏是一個科裏化函數的調用, 參數 dispatch 是原始,沒有進過改造的
        _dispatch = compose(...chain)(dispatch);

        return {
          dispatch: _dispatch,
          getState,
          subscription,
        }
      }
    }
  }
複製代碼

到這裏爲止,reduxjs 就基本實現了。可是咱們的探討尚未結束,繼續往下看

從上面的代碼咱們能夠看出來,applyMiddlyWare 函數其實就是對 createStore 的一層封裝,最終輸出的 dispatch 是通過中間件改造過的。如今咱們來看看這個 dispatch 究竟是什麼,它和咱們傳入的中間件有什麼關係???

中間件原理

const chain = middlewares.map(middleware => middleware({ dispatch: disp, getState }));
  _dispatch = compose(...chain)(dispatch);
複製代碼

上面的兩行代碼,先遍歷執行中間件,再將變量 chain 傳遞給 compose 函數。因此咱們應該能夠猜想到,表達式 middleware({ dispatch: disp, getState }) 應該返回一個函數,否則 compose 中的 reduce 就沒有辦法執行了。

這裏還要考慮到中間件執行的策略,全部的中間件必須串聯起來,挨個往下執行。因此中間件應該還應該接收另外一箇中間件做爲參數。因此如今咱們能夠大體的猜想到一箇中間件應該是這樣的:

function middleware({ dispatch, getState }) {
    return function (nextMiddleware) {
      return function () {
        // 這裏應該先執行一些任務,而後再去執行下一個中間件
        ...
        nextMiddleware();
      }
    }
  }
複製代碼

這個時候其實中間件的模型還不夠完整,少了一些東西。少了什麼了,就是 action 呀!applyMiddlyWare 函數經過中間件對 dispatch 進行改造。因此仍是要接收 action 才能對 state 進行修改。因此這下咱們清楚了

function middleware({ dispatch, getState }) {
    return function (nextMiddleware) {
      return function (action) {
        // 在調用 nextMiddleware 以前能夠進行一些操做
        console.log(1111);
        // 必須將 action 傳遞給下一個中間件
        const result = nextMiddleware(action);
        // 在調用 nextMiddleware 以後能夠進行一些操做
        console.log(222);
        return result;
      }
    }
  }
複製代碼

改造後的 dispatch 具體是個啥

如今咱們清楚了中間件的模型了,能夠來專門研究一下 applyMiddlyWare 函數返回的 dispatch 是啥玩意了

function compose() {
    const middleware = [...arguments];
    return middleware.reduce(function(a, b) {
      return function fn () {
        return a(b.apply(null, arguments));
      }
    });
  }
  function one(next) {
    console.log('one');
    return function one_(action) {
      console.log('這是中間件one,你能夠在這裏作不少事情', action);
      return next(action)
    }
  }
  function two(next) {
    console.log('two');
    return function two_(action) {
      console.log('這是中間件two,你能夠在next調用以前作一些事情', action);
      const result = next(action);
      console.log('這是中間件two,也能夠在next調用以後作一些事情', action);
      return result;
    }
  }
  function three(next) {
    console.log('three');
    return function three_(action) {
      console.log('這是中間件three,你能夠在這裏作不少事情', action);
      return next(action)
    }
  }
  // 能夠把它看成 createStore 函數返回的 dispatch 方法
  function dispatch(action) {
    console.log(action);
  }

  // 我這麼寫,你們應該能夠理解哈。由於 compose 函數接收到的實際上是 middleware({ dispatch, getState }) 返回的結果
  // 因此這裏的 one, two, three 能夠理解爲是 middleware({ dispatch, getState }) 返回的結果
  // 這裏只是作一個簡單的 demo,用不到 dispatch, getState。
  var disp = compose(one, two, three)(dispatch);
複製代碼

咱們把 compose(one, two, three)(dispatch) 這段代碼用咱們本身的代碼實現一下,大體就是下面這樣的效果:

var fn = (function(one, two, three) {
    var first = function() {
      return one(two.apply(null, arguments));
    };
    
    var next = function() {
      return first(three.apply(null, arguments));
    };
    return next
  })(one, two, three);

  var disp = fn(dispatch);
複製代碼
  • 當調用 fn(dispatch) 時,three.apply(null, dispatch) 開始執行,返回一個 three_ 函數。繼續往下執行。

  • first(three_) 開始執行,而後執行 two.apply(null, three_)two 執行完成,返回一個 two_ 函數。繼續往下執行。

  • one(two_) 開始執行,並返回一個 one_ 函數,這個函數最終做爲 fn(dispatch) 執行的最終結果,並賦值給變量 disp

disp(action) 執行時,先調用 one_(action) 而後是 two_(action) 最後是 three_(action)注意最後一箇中間件接收的參數不是中間件參數了,而是原始的 dispatch 方法。因此會在最後一箇中間件中執行 dispatch(action),從而調用 rducer 函數修改數據源【state】。

執行 disp({data: 1200, type: 'username'})這段代碼,看下打印的結果是啥

這下咱們就很是清楚了,原來通過 applyMiddlyWare 改造後輸出的 dispatch 方法,在調用時,會挨個執行每個傳入 applyMiddlyWare 函數的中間件,並在最後一箇中間件中調用原始的 dispatch() 方法。

最後本身實現一個 reduxjs 的應用

中間件定義

// 中間件1
  function thunk ({dispatch, getState}) {
    return function (next) {
      return function(action) {
        if (typeof action === 'function') {
          action({dispatch, getState});
        } else {
          return next(action);
        }
      } 
    }
  }
  // 中間件2
  function dialog ({dispatch, getState}) {
    return function (next) {
      return function(action) {
        console.log('prevstate:', getState());
        const result = next(action);
        console.log('nextstate:', getState());
        return result;
      }
    }
  }
複製代碼

effects 方法定義

// 模擬用戶http請求
  function getUserName(name) {
    return ({dispatch}) => {
      setTimeout(() => {
        dispatch({type: 'name', data: name})
      }, 0);
    }
  }
  function getUserAge(age) {
    return ({dispatch}) => {
      setTimeout(() => {
        dispatch({type: 'age', data: age})
      }, 0);
    }
  }
  function getUserSex(sex) {
    return ({dispatch}) => {
      setTimeout(() => {
        dispatch({type: 'sex', data: sex})
      }, 0);
    }
  }
  function getHome(value) {
    return ({dispatch}) => {
      setTimeout(() => {
        dispatch({type: 'home', data: value})
      }, 0);
    }
  }
  function getList(value) {
    return ({dispatch}) => {
      setTimeout(() => {
        dispatch({type: 'list', data: value})
      }, 0);
    }
  }
  function getDetail(value) {
    return ({dispatch}) => {
      setTimeout(() => {
        dispatch({type: 'detail', data: value})
      }, 0);
    }
  }
複製代碼

初始化 state, 綁定到 DOM

// userName, menu 直接複製前面的代碼
  var reducer = combineReducers({ userName, menu });

  var { dispatch, getState, subscription } = applyMiddlyWare(store)(reducer)(thunk, dialog);
  console.log(getState(), 'initState');

  const name_button = document.querySelector('.name');
  const age_button = document.querySelector('.age');
  const sex_button = document.querySelector('.sex');
  const home_button = document.querySelector('.home');
  const list_button = document.querySelector('.list');
  const detail_button = document.querySelector('.detail');
  const addListener = document.querySelector('.addListener');
  const removeListener = document.querySelector('.removeListener');

  name_button.onclick = function() {
    dispatch(getUserName('shenxuxiang'))
  };

  age_button.onclick = function() {
    dispatch(getUserAge('29'))
  };

  sex_button.onclick = function() {
    dispatch(getUserSex('man'))
  };

  home_button.onclick = function() {
    dispatch(getHome('home_page'))
  };

  list_button.onclick = function() {
    dispatch(getList('list_page'))
  };

  detail_button.onclick = function() {
    dispatch(getDetail('detail_page'))
  };

  let removeListen;
  addListener.onclick = function() {
    removeListen = subscription(function() {
      console.log('咱們添加了一個事件監聽器', getState())
    })
  };

  removeListener.onclick = function() {
    removeListen && removeListen();
  };
複製代碼

最後,要過年了,祝你們新年快樂。

相關文章
相關標籤/搜索