精讀《重新思考 Redux》

黃子毅發表於2018-05-14

本週精讀內容是 《重新思考 Redux》

1 引言

《重新思考 Redux》是 rematch 作者 Shawn McKay 寫的一篇乾貨軟文。

dva 之後,有許多基於 redux 的狀態管理框架,但大部分都很侷限,甚至是倒退。但直到看到了 rematch,總算覺得 redux 社群又進了一步。

這篇文章的寶貴之處在於,拋開 Mobx、RXjs 概念,僅針對 redux 做深入的重新思考,對大部分還在使用 redux 的工程場景非常有幫助。

2 概述

比較新穎的是,作者給出一個公式,評價一個框架或工具的質量:

工具質量 = 工具節省的時間/使用工具消耗的時間

如果這樣評估原生的 redux,我們會發現,使用 redux 需要額外花費的時間可能超過了其節省下來的時間,從這個角度看,redux 是會降低工作效率的。

但 redux 的資料管理思想是正確的,複雜的前端專案也確實需要這種理念,為了更有效率的使用 redux,我們需要使用基於 redux 的框架。作者從 6 個角度闡述了基於 redux 的框架需要解決什麼問題。

簡化初始化

redux 初始化程式碼涉及的概念比較多,比如 compose thunk 等等,同時將 reducerinitialStatemiddlewares 這三個重要概念拆分成了函式方式呼叫,而不是更容易接受的配置方式:

const store = preloadedState => {
  return createStore(
    rootReducer,
    preloadedState,
    compose(applyMiddleware(thunk, api), DevTools.instrument())
  );
};
複製程式碼

如果換成配置方式,理解成本會降低不少:

const store = new Redux.Store({
  instialState: {},
  reducers: { count },
  middlewares: [api, devTools]
});
複製程式碼

筆者注:redux 的初始化方式非常函式式,而下面的配置方式就更物件導向一些。相比之下,還是物件導向的方式更好理解,畢竟 store 是一個物件。instialState 也存在同樣問題,相比顯示申明,將 preloadedState 作為函式入參就比較抽象了,同時 redux 對初始 state 的賦值也比較隱蔽,createStore 時統一賦值比較彆扭,因為 reducers 是分散的,如果在 reducers 中賦值,要利用 es 的預設引數特性,看起來更像業務思考,而不是 redux 提供的能力。

簡化 Reducers

redux 的 reducer 粒度太大,不但導致函式內手動匹配 type,還帶來了 typepayload 等理解成本:

const countReducer = (state, action) => {
  switch (action.type) {
    case INCREMENT:
      return state + action.payload;
    case DECREMENT:
      return state - action.payload;
    default:
      return state;
  }
};
複製程式碼

如果用配置的方式設定 reducers,就像定義一個物件一樣,會更清晰:

const countReducer = {
  INCREMENT: (state, action) => state + action.payload,
  DECREMENT: (state, action) => state - action.payload
};
複製程式碼

支援 async/await

redux 支援動態資料還是挺費勁的,需要理解高階函式,理解中介軟體的使用方式,否則你不會知道為什麼這樣寫是對的:

const incrementAsync = count => async dispatch => {
  await delay();
  dispatch(increment(count));
};
複製程式碼

為什麼不抹掉理解成本,直接允許 async 型別的 action 呢?

const incrementAsync = async count => {
  await delay();
  dispatch(increment(count));
};
複製程式碼

筆者注:我們發現 rematch 的方式,dispatch 是 import 進來的(全域性變數),而 redux 的 dispatch 是注入進來的,乍一看似乎 redux 更合理,但其實我更推崇 rematch 的方案。經過長期實踐,元件最好不要使用資料流,專案的資料流只用一個例項完全夠用了,全域性 dispatch 的設計其實更合理,而注入 dispatch 的設計看似追求技術極致,但忽略了業務使用場景,導致畫蛇添足,增加了不必要的麻煩。

將 action + reducer 改為兩種 action

redux 抽象的 action 與 reducer 的指責很清晰,action 負責改 store 以外所有事,而 reducer 負責改 store,偶爾用來做資料處理。這種概念其實比較模糊,因為往往不清楚資料處理放在 action 還是 reducer 裡,同時過於簡單的 reducer 又要寫 action 與之匹配,感覺過於形式化,而且繁瑣。

重新考慮這個問題,我們只有兩類 action:reducer actioneffect action

  • reducer action:改變 store。
  • effect action:處理非同步場景,能呼叫其他 action,不能修改 store。

同步的場景,一個 reducer 函式就能處理,只有非同步場景需要 effect action 處理掉非同步部分,同步部分依然交給 reducer 函式,這兩種 action 職責更清晰。

不再顯示申明 action type

不要在用一個檔案儲存 Action 型別了,const ACTION_ONE = 'ACTION_ONE' 其實重複寫了一遍字串,直接用物件的 key 表示 action 的值,再加上 store 的 name 為字首保證唯一性即可。

同時 redux 建議使用 payload key 來傳值,那為什麼不強制使用 payload 作為入參,而要通過 action.payload 取值呢?直接使用 payload 不但視覺上減少程式碼數量,容易理解,同時也強制約束了程式碼風格,讓建議真正落地。

Reducer 直接作為 ActionCreator

redux 呼叫 action 比較繁瑣,使用 dispatch 或者將 reducer 經過 ActionCreator 函式包裝。為什麼不直接給 reducer 自動包裝 ActionCreator 呢?減少樣板程式碼,讓每一行程式碼都有業務含義。

最後作者給出了一個 rematch 完整的例子:

import { init, dispatch } from "@rematch/core";
import delay from "./makeMeWait";

const count = {
  state: 0,
  reducers: {
    increment: (state, payload) => state + payload,
    decrement: (state, payload) => state - payload
  },
  effects: {
    async incrementAsync(payload) {
      await delay();
      this.increment(payload);
    }
  }
};

const store = init({
  models: { count }
});

dispatch.count.incrementAsync(1);
複製程式碼

3 精讀

我覺得本文基本上把 redux 存在的工程問題分析透徹了,同時還給出了一套非常好的實現。

細節的極致優化

首先是直接使用 payload 而不是整個 action 作為入參,加強了約束同時簡化程式碼複雜度:

increment: (state, payload) => state + payload;
複製程式碼

其次使用 async 在 effects 函式中,使用 this.increment 函式呼叫方式,取代 put({type: "increment"})(dva),在 typescript 中擁有了型別支援,不但可以用自動跳轉代替字串搜尋,還能校驗引數型別,在 redux 框架中非常難得。

最後在 dispatch 函式,也提供了兩種呼叫方式:

dispatch({ type: "count/increment", payload: 1 });
dispatch.count.increment(1);
複製程式碼

如果為了更好的型別支援,或者遮蔽 payload 概念,可以使用第二種方案,再一次簡化 redux 概念。

內建了比較多的外掛

rematch 將常用的 reselect、persist、immer 等都整合為了外掛,相對比較強化外掛生態的概念。資料流對資料快取,效能優化,開發體驗優化都有進一步施展的空間,擁抱外掛生態是一個良好的發展方向。

比如 rematch-immer 外掛,可以用 mutable 的方式修改 store:

const count = {
  state: 0,
  reducers: {
    add(state) {
      state += 1;
      return state;
    }
  }
};
複製程式碼

但是當 state 為非物件時,immer 將不起作用,所以最好能養成 return state 的習慣。

最後說一點瑕疵的地方,reducers 申明與呼叫引數不一致。

Reducers 申明與呼叫引數不一致

比如下面的 reducers:

const count = {
  state: 0,
  reducers: {
    increment: (state, payload) => state + payload,
    decrement: (state, payload) => state - payload
  },
  effects: {
    async incrementAsync(payload) {
      await delay();
      this.increment(payload);
    }
  }
};
複製程式碼

定義時 increment 是兩個引數,而 incrementAsync 呼叫它時,只有一個引數,這樣可能造成一些誤導,筆者建議保持引數對應關係,將 state 放在 this 中:

const count = {
  state: 0,
  reducers: {
    increment: payload => this.state + payload,
    decrement: payload => this.state - payload
  },
  effects: {
    async incrementAsync(payload) {
      await delay();
      this.increment(payload);
    }
  }
};
複製程式碼

當然 rematch 的方式保持了函式的無副作性質,可以看出是做了一些取捨。

4 總結

重複一下作者提出工具質量的公式:

工具質量 = 工具節省的時間/使用工具消耗的時間

如果一個工具能節省開發時間,但本身帶來了很大使用成本,在想清楚如何減少使用成本之前,不要急著用在專案中,這是我得到的最大啟發。

最後感謝 rematch 作者精益求精的精神,給 redux 帶來進一步的極致優化。

5 更多討論

討論地址是:精讀《重新思考 Redux》 · Issue #83 · dt-fe/weekly

如果你想參與討論,請點選這裡,每週都有新的主題,週末或週一釋出。

相關文章