在 React 中處理數據流問題的一些思考

背景

相信你們在項目開發中,在頁面較複雜的狀況下,每每會遇到一個問題,就是在頁面組件之間通訊會很是困難。ios

好比說一個商品列表和一個已添加商品列表:git

假如這兩個列表是獨立的兩個組件,它們會共享一個數據 「被選中的商品」,在商品列表選中一個商品,會影響已添加商品列表,在已添加列表中刪除一個商品,一樣會影響商品列表的選中狀態。github

它們兩個是兄弟組件,在沒有數據流框架的幫助下,在組件內數據有變化的時候,只能經過父組件傳輸數據,每每會有 onSelectedDataChange 這種函數出現,在這種狀況下,還尚且能忍受,若是組件嵌套較深的話,那痛苦能夠想象一下,因此纔有解決數據流的各類框架的出現。redux

本質分析

咱們知道 React 是 MVC 裏的 V,而且是數據驅動視圖的,簡單來講,就是數據 => 視圖,視圖是基於數據的渲染結果:axios

V = f(M)
複製代碼

數據有更新的時候,在進入渲染以前,會先生成 Virtual DOM,先後進行對比,有變化才進行真正的渲染。api

V + ΔV = f(M + ΔM)
複製代碼

數據驅動視圖變化有兩種方式,一種是 setState,改變頁面的 state,一種是觸發 props 的變化。bash

咱們知道數據是不會本身改變,那麼確定是有「外力」去推進,每每是遠程請求數據回來或者是 UI 上的交互行爲,咱們統稱這些行爲叫 actionapp

ΔM = perform(action) 
複製代碼

每個 action 都會去改變數據,那麼視圖獲得的數據(state)就是全部 action 疊加起來的變動,框架

state = actions.reduce(reducer, initState)
複製代碼

因此真實的場景會出現以下或更復雜的狀況:異步

問題就出在,更新數據比較麻煩,混亂,每次要更新數據,都要一層層傳遞,在頁面交互複雜的狀況下,沒法對數據進行管控。

有沒有一種方式,有個集中的地方去管理數據,集中處理數據的接收修改分發?答案顯然是有的,數據流框架就是作這個事情,熟悉 Redux 的話,就知道其實上面講的就是 Redux 的核心理念,它和 React 的數據驅動原理是相匹配的。

數據流框架

Redux

數據流框架目前佔主要地位的仍是 Redux,它提供一個全局 Store 處理應用數據的接收修改分發

它的原理比較簡單,View 裏面有任何交互行爲須要改變數據,首先要發一個 action,這個 actionStore 接收並交給對應的 reducer 處理,處理完後把更新後的數據傳遞給 ViewRedux 不依賴於任何框架,它只是定義一種方式控制數據的流轉,能夠應用於任何場景。

雖然定義了一套數據流轉的方式,但真正使用上會有很多問題,我我的總結主要是兩個問題:

  1. 定義過於繁瑣,文件多,容易形成思惟跳躍。
  2. 異步流的處理沒有優雅的方案。

咱們來看看寫一個數據請求的例子,這是很是典型的案例:

actions.js

export const FETCH_DATA_START = 'FETCH_DATA_START';
export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS';
export const FETCH_DATA_ERROR = 'FETCH_DATA_ERROR';

export function fetchData() {
  return dispatch => {
    dispatch(fetchDataStart());
    axios.get('xxx').then((data) => {
      dispatch(fetchDataSuccess(data));
    }).catch((error) => {
      dispatch(fetchDataError(error));
    });
  };
}

export function fetchDataStart() {
  return {
    type: FETCH_DATA_START,
  }
}

...FETCH_DATA_SUCCESS
...FETCH_DATA_ERROR

複製代碼

reducer.js

import { FETCH_DATA_START, FETCH_DATA_SUCCESS, FETCH_DATA_ERROR  } from 'actions.js';
export default (state = { data: null }, action) => {
  switch (action.type) {
    case FETCH_DATA_START:
      ...
    case FETCH_DATA_SUCCESS:
      ...
    case FETCH_DATA_ERROR:
      ...
    default: 
      return state
  }
}
複製代碼

view.js

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from 'reducer.js';
import { fetchData } from 'actions.js';

const store = createStore(reducer, applyMiddleware(thunk));
store.dispatch(fetchData());
複製代碼

第一個問題,發一個請求,由於須要託管請求的全部狀態,因此須要定義不少的 action,這時很容易會繞暈,就算有人嘗試把這些狀態再封裝抽象,也會充斥着一堆模板代碼。有人會挑戰說,雖然一開始是比較麻煩,繁瑣,但對項目可維護性,擴展性都比較友好,我不太認同這樣的說法,目前還算簡單,真正業務邏輯複雜的狀況下,會顯得更噁心,效率低且閱讀體驗差,相信你們也寫過或看過這樣的代碼,後面本身看回來,須要在 actions 文件搜索一下 action 的名稱,reducer 文件查詢一下,繞一圈才慢慢看懂。

第二個問題,按照官方推薦使用 redux-thunk 實現異步 action 的方法,只要在 action 裏返回一個函數便可,這對有強迫症的人來講,簡直受不了,actions 文件顯得它很不純,原本它只是來定義 action,卻居然要夾雜着數據請求,甚至 UI 上的交互!

我以爲 Redux 設計上沒有問題,思路很是簡潔,是我很是喜歡的一個庫,它提供的數據的流動方式,目前也是獲得社區的普遍承認。然而在使用上有它的缺陷,雖然是能夠克服,可是它自己難道沒有能夠優化的地方?

dva

dva 的出來就是爲了解決 redux 的開發體驗問題,它首次提出了 model 的概念,很好地把 actionreducersstate 結合到一個 model 裏面。

model.js

export default {
  namespace: 'products',
  state: [],
  reducers: {
    'delete'(state, { payload: id }) {
      return state.filter(item => item.id !== id);
    },
  },
};
複製代碼

它的核心思想就是一個 action 對應一個 reducer,經過約定,省略了對 action 的定義,默認 reducers 裏面的函數名稱即爲 action 的名稱。

在異步 action 的處理上,定義了 effects(反作用) 的概念,與同步 action 區分起來,內部藉助了 redux-saga 來實現。

model.js

export default {
  namespace: 'counter',
  state: [],
  reducers: {
  },
  effects: {
    *add(action, { call, put }) {
      yield call(delay, 1000);
      yield put({ type: 'minus' });
    },
  },
};
複製代碼

經過這樣子的封裝,基本保持 Redux 的用法,咱們能夠沉浸式地在 model 編寫咱們的數據邏輯,我以爲已經很好地解決問題了。

不過我我的喜愛問題,不太喜歡使用 redux-saga 這個庫來解決異步流,雖然它的設計很巧妙,利用了 generator 的特性,不侵入 action,而是經過中間件的方式進行攔截,很好地將異步處理隔離出獨立的一層,而且以此聲稱對實現單元測試是最友好的。是的,我以爲設計上真的很是棒,那時候還特地閱讀了它的源碼,讚歎做者真的牛,這樣的方案都能想出來,可是後來我看到還有更好的解決方案(後面會介紹),就放棄使用它了。

mirrorx

mirrorxdva 差很少,只是它使用了單例的方式,全部的 action 都保存了 actions 對象中,訪問 action 有了另外一種方式。還有就是處理異步 action 的時候可使用 async/await 的方式。

import mirror, { actions } from 'mirrorx'

mirror.model({
  name: 'app',
  initialState: 0,
  reducers: {
    increment(state) { return state + 1 },
    decrement(state) { return state - 1 }
  },
  effects: {
    async incrementAsync() {
      await new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve()
        }, 1000)
      })
      actions.app.increment()
    }
  }
});
複製代碼

它內部處理異步流的問題,相似 redux-thunk 的處理方式,經過注入一箇中間件,這個中間件裏判斷 當前 action 是否是異步 action(只要判斷是否是 effects 裏定義的 action 便可),若是是的話,就直接中斷了中間件的鏈式調用,能夠看看這段代碼

這樣的話,咱們 effects 裏的函數就可使用 async/await 的方式調用異步請求了,其實不是必定要使用 async/await,函數裏的實現沒有限制,由於中間件只是調用函數執行而已。

我是比較喜歡使用 async/await 這種方式處理異步流,這是我不用 redux-saga 的緣由。

xredux

可是我最終沒有選擇使用 mirrorxdva,由於用它們就捆綁一堆東西,我以爲不該該作成這樣子,爲啥好好的解決 Redux 問題,最後變成都作一個腳手架出來?這不是強制消費嗎?讓人用起來就會有限制。瞭解它們的原理後,我本身參照寫了個 xredux 出來,只是單純解決 Reudx 的問題,不依賴於任何框架,能夠看做只是 Redux 的升級版。

使用上和 mirrorx 差很少,但它和 Redux 是同樣的,不綁定任何框架,能夠獨立使用。

import xredux from "xredux";

const store = xredux.createStore();
const actions = xredux.actions;

// This is a model, a pure object with namespace, initialState, reducers, effects.
xredux.model({
  namespace: "counter",
  initialState: 0,
  reducers: {
    add(state, action) { return state + 1; },
    plus(state, action) { return state - 1; },
  },
  effects: {
    async addAsync(action, dispatch, getState) {
      await new Promise(resolve => {
        setTimeout(() => {
          resolve();
        }, 1000);
      });
      actions.counter.add();
    }
  }
});

// Dispatch action with xredux.actions
actions.counter.add();
複製代碼

在異步處理上,其實也存在問題,可能你們也遇到過,就是數據請求有三種狀態的問題,咱們來看看,寫一個數據請求的 effects

import xredux from 'xredux';
import { fetchUserInfo } from 'services/api';

const { actions } = xredux;

xredux.model({
  namespace: 'user',
  initialState: {
    getUserInfoStart: false,
    getUserInfoError: null,
    userInfo: null,
  },
  reducers: {
    // fetch start
    getUserInfoStart (state, action) {
      return {
        ...state,
        getUserInfoStart: true,
      };
    },
    // fetch error
    getUserInfoError (state, action) {
      return {
        ...state,
        getUserInfoStart: false,
        getUserInfoError: action.payload,
      };
    },
    // fetch success
    setUserInfo (state, action) {
      return {
        ...state,
        userInfo: action.payload,
        getUserInfoStart: false,
      };
    }
  },
  effects: {
    async getUserInfo (action, dispatch, getState) {
      let userInfo = null;
      actions.user.getUserInfoStart();
      try {
        userInfo = await fetchUserInfo();
        actions.user.setUserInfo(userInfo);
      } catch (e) {
        actions.user.setUserInfoError(e);
      }
    }
  },
});
複製代碼

能夠看到,仍是存在不少感受沒用的代碼,一個請求須要3個 reducer 和1個 effect,當時想着怎麼優化,但沒有很好的辦法,後來我想到這3個 reducer 有個共同點,就是隻是賦值,沒有任何操做,那我內置一個 setStatereducer,專門去處理這種只是賦值的 action 就行了。

最後變成這樣:

import xredux from 'xredux';
import { fetchUserInfo } from 'services/api';

const { actions } = xredux;

xredux.model({
  namespace: 'user',
  initialState: {
    getUserInfoStart: false,
    getUserInfoError: null,
    userInfo: null,
  },
  reducers: {
  },
  effects: {
    async getUserInfo (action, dispatch, getState) {
      let userInfo = null;
      // fetch start
      actions.user.setState({
        getUserInfoStart: true,
      });
      try {
        userInfo = await fetchUserInfo();
        // fetch success
        actions.user.setState({
          getUserInfoStart: false,
          userInfo,
        });
      } catch (e) {
        // fetch error
        actions.user.setState({
          getUserInfoError: e,
        });
      }
    }
  },
});
複製代碼

這個目前是本身比較滿意的方案,在項目中也有實踐過,寫起來確實比較簡潔易懂,不知你們有沒有更好的辦法。

貧血組件/充血組件

使用了 Redux,按道理應用中的狀態數據應該都放到 Store 中,那組件是否能有本身的狀態呢?目前就會有兩種見解:

  • 全部狀態都應該在 Store 中託管,全部組件都是純展現組件。
  • 組件可擁有本身的部分狀態,另一些由 Store 託管。

這兩種就是分別對應貧血組件和充血組件,區別就是組件是否有本身的邏輯,仍是說只是純展現。我以爲這個問題不用去爭論,沒有對錯。

理論上固然是說貧血組件好,由於這樣保證數據是在一個地方管理的,可是付出的代價多是沉重的,使用了這種方式,每每到後面會有想死的感受,一種想回頭又不想放棄的感受,其實不必這麼執着。

相信你們幾乎都是充血組件,有一些狀態只與組件相關的,由組件去託管,有些狀態須要共享的,交給 Store 去託管,甚至有人全部狀態都有組件託管,也是存在的,由於頁面太簡單,根本就不須要用到數據流框架。

總結

React 開發中不可避免會遇到數據流的問題,如何優雅地處理目前也沒有最完美的方案,社區也存在各類各樣的方法,能夠多思考爲何是這樣作,瞭解底層原理比盲目使用別人的方案更重要。

若是想詳細瞭解 xredux 如何在 React 中運用,可使用 RIS 初始化一個 Standard 應用看看,以前的文章《RIS,建立 React 應用的新選擇》 有簡單提過,歡迎你們體驗。

參考資料

相關文章
相關標籤/搜索