Redux中介軟體對閉包的一個巧妙使用

艾特老幹部發表於2019-02-28

最近在看Redux的原始碼,發現Redux在使用中介軟體applyMiddleware.js的原始碼中,有一個對閉包非常巧妙的使用,解決了“雞生蛋,蛋生雞”的問題,特分享給大家。

Redux中介軟體的函式簽名形式如下:

({dispatch, getState}) => next => action => {
   // 函式體
}
複製程式碼

applyMiddleware.js中的函式applyMiddleware(...middlewares)用於根據中介軟體生成action經過的中介軟體鏈。先來看一個錯誤版本的實現:

/*
 * @param {...Function} middlewares The middleware chain to be applied.
 * @returns {Function} A store enhancer applying the middleware.
 */
export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, initialState, enhancer) => {
    var store = createStore(reducer, initialState, enhancer)
    var chain = []

    var middlewareAPI = {
      getState: store.getState,
      dispatch: store.dispatch
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    var dispatch = compose(...chain)(store.dispatch)    //compose(f, g, h) 等價於函式
                                                    //(...args)=>f(g(h(args)))

    return {
      ...store,
      dispatch
    }
  }
複製程式碼

核心邏輯是chain = middlewares.map(middleware => middleware(middlewareAPI))和dispatch = compose(...chain)(store.dispatch)這兩行。第1句程式碼是根據中介軟體生成一個陣列chain,chain的元素是簽名為next => action => {...}形式的函式,每個元素就是最終中介軟體鏈上的一環。第2句程式碼利用compose函式,將chain中的函式元素組成一個“洋蔥式”的大函式,chain的每個函式元素相當於一層洋蔥表皮。Redux傳送的每一個action都會由外到內依次經過每一層函式的處理。假設有3層函式,從外到內依次是a,b,c,函式的實際呼叫過程是,a接收到action,在a函式體內會呼叫b(a的引數next,指向的就是b),並把action傳遞給b,然後b呼叫c(b的引數next指向的就是c),同時也把action傳遞給c,c的引數next指向的是原始的store.dispatch,因此是action dispatch的最後一環。這樣分析下來,程式是沒有問題的,但當我們的中介軟體需要直接使用dispatch函式時,問題就出來了。例如,常用於傳送非同步action的中介軟體redux-thunk,就需要在非同步action中使用dispatch:

export function fetchPosts(subreddit) {  
  return function (dispatch) {
    dispatch(requestPosts(subreddit))
    return fetch(`https://www.reddit.com/r/${subreddit}.json`)
      .then(
        response => response.json(),
        error => console.log('An error occured.', error)
      )
      .then(json =>
        dispatch(receivePosts(subreddit, json))
      )
  }
}
複製程式碼

fetchPosts使用的dispatch,是redux-thunk傳遞過來的,指向的是middlewareAPI物件中的dispatch,實際等於store.dispatch。當執行dispatch(requestPosts(subreddit))時,這個action直接就到了最後一環節的處理,跳過了redux-thunk中介軟體之後的其他中介軟體的處理,顯然是不合適的。我們希望的方式是,這個action依然會從最外層的中介軟體開始,由外到內經過每一層中介軟體的處理。所以,這裡使用的dispatch函式不能等於store.dispatch,應該等於compose(...chain)(store.dispatch),只有這樣,傳送的action才能經過每一層中介軟體的處理。現在問題出來了,chain = middlewares.map(middleware => middleware(middlewareAPI))需要使用dispatch = compose(...chain)(store.dispatch)返回的dispatch函式,而dispatch = compose(...chain)(store.dispatch)的執行又依賴於chain = middlewares.map(middleware => middleware(middlewareAPI))的執行結果,我們進入死迴圈了。

問題的解決方案就是閉包。當我們定義middlewareAPI的dispatch時,不直接把它指向store.dispatch,而是定義一個新的函式,在函式中引用外部的一個區域性變數dispatch,這樣就形成了一個閉包,外部dispatch變數的變化會同步反映到內部函式中。如下所示:

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, initialState, enhancer) => {
    var store = createStore(reducer, initialState, enhancer)
    var dispatch = store.dispatch;   // 需要有初始值,保證中介軟體在初始化過程中也可以正常使用dispatch
    var chain = []

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)    // 通過閉包引用外部的dispatch變數
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)    //compose(f, g, h) 等價於函式
                                                    //(...args)=>f(g(h(args)))

    return {
      ...store,
      dispatch
    }
  }
複製程式碼

這樣,“雞生蛋,蛋生雞”的問題就解決了。如果這個例子對你來說太複雜,可以用下面這個簡化的例子幫助你理解:

const middleware = ({dispatch}) => (next) => (number) => {
  console.log("in middleware");
  if(number !== 0){
    return dispatch(--number);
  }
  
  return next(number);
}

function test() {
  var dispatch = (number) => { 
    console.log("original dispatch");
    return number;
  };
  var middlewareAPI = {
    dispatch
  }
  
  dispatch = middleware(middlewareAPI)(dispatch);
  
  return {
    dispatch
  }
}

var {dispatch} = test();
dispatch(3);

//輸出:
"in middleware"
"original dispatch"

const middleware = ({dispatch}) => (next) => (number) => {
  console.log("in middleware");
  if(number !== 0){
    return dispatch(--number);
  }
  
  return next(number);
}

function test() {
  var dispatch = (number) => { 
    console.log("original dispatch");
    return number;
  };
  var middlewareAPI = {
    dispatch: (number) => {dispatch(number);}
  }
  
  dispatch = middleware(middlewareAPI)(dispatch);
  
  return {
    dispatch
  }
}

var {dispatch} = test();
dispatch(3);

//輸出 
"in middleware"
"in middleware"
"in middleware"
"in middleware"
"original dispatch"
複製程式碼

第二種方式,middleware中dispatch的number會再次經歷中介軟體的處理,當number=3,2,1,0時,都會進入一次middleware函式,當number=0時,next(0)呼叫的是test中定義的初始dispatch函式,因此不再經過middleware的處理。


歡迎關注我的公眾號:老幹部的大前端,領取21本大前端精選書籍!

Redux中介軟體對閉包的一個巧妙使用

相關文章