精讀《從新思考 Redux》

本週精讀內容是 《從新思考 Redux》前端

1 引言

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

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

這篇文章的寶貴之處在於,拋開 Mobx、RXjs 概念,僅針對 redux 作深刻的從新思考,對大部分還在使用 redux 的工程場景很是有幫助。typescript

2 概述

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

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

若是這樣評估原生的 redux,咱們會發現,使用 redux 須要額外花費的時間可能超過了其節省下來的時間,從這個角度看,redux 是會下降工做效率的。緩存

但 redux 的數據管理思想是正確的,複雜的前端項目也確實須要這種理念,爲了更有效率的使用 redux,咱們須要使用基於 redux 的框架。做者從 6 個角度闡述了基於 redux 的框架須要解決什麼問題。性能優化

簡化初始化

redux 初始化代碼涉及的概念比較多,好比 compose thunk 等等,同時將 reducerinitialStatemiddlewares 這三個重要概念拆分紅了函數方式調用,而不是更容易接受的配置方式:app

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

若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。

相關文章
相關標籤/搜索