redux-thunk 之謎

LitileCoder發表於2019-04-01

如果你在用 redux 的話,那你估計也會用到 redux-thunk 這個庫。區別於作者的說法,以下是日了狗的想法。

thunk 求值

日了狗說:“你可能得先了解下 thunk 。”

程式語言早期,計算機學家在研究如何編譯時,對“求值策略”產生了爭執。舉個栗子:

const x = 1;

function foo(num) {
  return num * 2;
}

foo(x + 3);
複製程式碼

x + 3 該在何時計算?引發了兩種意見:

  • 傳值呼叫,在進入 foo 之間計算,等同於 foo(4)
  • 傳名呼叫,只在用到時求值,等同於(x + 3) * 2

兩種計算方式各有利弊,傳值呼叫相對簡單,但有可能根本沒用到,比如:

function bar(a, b) {
  return b + 2;
}

bar(1 + 2 * (3 - x) / x -4, x);
複製程式碼

定義的 bar 函式體內沒有用到 a ,根本無需計算,傳值呼叫可能會造成效能損失。所以有些計算機學家更傾向於傳名呼叫

thunk 求值就是基於傳名呼叫的編譯器實現。

const a = 1;
const thunk = function thunk(x) {
  return x + 3;
}

function foo(thunk) {
  return thunk() * 2;
}
複製程式碼

在 JavaScript 語言中,thunk 函式略有不同。也叫函式“柯里化”,一種函數語言程式設計的概念,將多引數轉化為單引數形式

function foo(var1, var2) {
  return var1 + var2;
}

function thunk(var1) {
  return function (var2) {
    return var1 + var2;
  };
}
// 看最多的是 ES6 的騷寫法
const thunk = var1 => var2 => var1 + var2;

// 實際上還是傳值呼叫
foo(1, 2) === thunk(1)(2);
複製程式碼

日了狗說:“好的。我們現在開始回到 redux-thunk 。”

為什麼要用

當你在 redux 中觸發 state 變化時,你肯定會這樣 dispatch(action)。處理同步程式碼當然沒問題,但是非同步程式碼呢?比如說非同步請求,emmmmm...跟著官方的程式碼走是這樣的:

function success(data) {
  return {
    type: 'SUCCESS',
    data,
  };
}

function asyncFunc(payload) {
  return dispatch => {
    fetch(url, payload)
      .then(res => res.json())
      .then(data => dispatch(success(data)))
  };
}

dispatch(asyncFunc({}));
複製程式碼

如果你沒用 redux-thunk 的話,你應該會收到一條錯誤提示。這是因為 redux 裡做了對 action 的校驗:

// 必須要是個包含 type 屬性的純物件
// 而 asyncFunc 返回的是個 Promise
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?'
  )
}
複製程式碼

聰明的你會想到另一種解決辦法。執行 Promise,dispatch 物件:

// store.js
const store = createStore();

// action.js
function success(data) {
  return {
    type: 'SUCCESS',
    data,
  };
}

// index.js
fetch(url, data)
  .then()
  .then(res => /* import store from store.js */ store.dispatch(success(res)));
複製程式碼

福兮禍所依,這樣子的寫法有幾個問題:

  1. 寫法不規範。createAction 不統一
  2. 保持對 store 的引用。汙染全域性、修改此物件都可能造成影響
  3. 重複程式碼。同一個請求多處寫

日了狗說:“ok,現在我們來看看 redux-thunk 做了什麼事。”

做了什麼事

直接看原始碼。只有區區不到 15 行的程式碼:

// redux-thunk 功能程式碼
const thunk = ({ dispatch, getState }) => next => action => {
  if (typeof action === 'function') {
    return action(dispatch, getState);
  }

  return next(action);
}
複製程式碼

光看這裡程式碼一臉懵。可能大家一直在糾結 next 是什麼,action 又是什麼。那不妨多糾結一下,看看 redux 的中介軟體怎麼應用的:

// 核心程式碼
function applyMiddleware(...middlewares) {
  const middlewareAPI = {
    getState: store.getState,
    dispatch: (...args) => dispatch(...args)
  }
  const chain = middlewares.map(middleware => middleware(middlewareAPI))

  dispatch = compose(...chain)(store.dispatch)
}
複製程式碼

主要做了兩件事:

  1. dispatch 注入中介軟體
  2. 將所有的中介軟體 compose 成一個 dispatch

redux 中的 compose 函式很有意思,將多個函式整合成一個,按順序同步執行,上一個函式的返回值為下一個函式的入參。詳情可看原始碼

Tip: 如果看原始碼不容易理解的話,可以嘗試看下測試用例

瞭解了 applyMiddleware 後,再應用 redux-thunk,你就會發現原本的 dispatch 函式被改變了。在 action 觸發之間會先執行一遍 redux-thunk 功能程式碼,判斷傳入的 action 是否為函式型別,如果是,則注入 dispatch,執行此函式

簡而言之,redux-thunk 直接讓 dispatch 跳過了 action 型別驗證,然後就沒啥了。。。

最後,日了狗覺得事情並不簡單。

相關文章