Vuex、Flux、Redux、Redux-saga、Dva、MobX

這篇文章試着聊明白這一堆看起來挺複雜的東西。在聊之前,大家要始終記得一句話:一切前端概念,都是紙老虎

不管是Vue,還是 React,都需要管理狀態(state),比如組件之間都有共享狀態的需要。什麼是共享狀態?比如一個組件需要使用另一個組件的狀態,或者一個組件需要改變另一個組件的狀態,都是共享狀態。

父子組件之間,兄弟組件之間共享狀態,往往需要寫很多沒有必要的代碼,比如把狀態提升到父組件裏,或者給兄弟組件寫一個父組件,聽聽就覺得挺囉嗦。

如果不對狀態進行有效的管理,狀態在什麼時候,由於什麼原因,如何變化就會不受控制,就很難跟蹤和測試了。如果沒有經歷過這方面的困擾,可以簡單理解爲會搞得很亂就對了

在軟件開發裏,有些通用的思想,比如隔離變化,約定優於配置等,隔離變化就是說做好抽象,把一些容易變化的地方找到共性,隔離出來,不要去影響其他的代碼。約定優於配置就是很多東西我們不一定要寫一大堆的配置,比如我們幾個人約定,view 文件夾裏只能放視圖,不能放過濾器,過濾器必須放到 filter 文件夾裏,那這就是一種約定,約定好之後,我們就不用寫一大堆配置文件了,我們要找所有的視圖,直接從 view 文件夾裏找就行。

根據這些思想,對於狀態管理的解決思路就是:把組件之間需要共享的狀態抽取出來,遵循特定的約定,統一來管理,讓狀態的變化可以預測。根據這個思路,產生了很多的模式和庫,我們來挨個聊聊。

Store 模式

最簡單的處理就是把狀態存到一個外部變量裏面,比如:this.$root.$data,當然也可以是一個全局變量。但是這樣有一個問題,就是數據改變後,不會留下變更過的記錄,這樣不利於調試。

所以我們稍微搞得複雜一點,用一個簡單的 Store 模式:

var store = {
  state: {
    message: 'Hello!'
  },
  setMessageAction (newValue) {
    // 發生改變記錄點日誌啥的
    this.state.message = newValue
  },
  clearMessageAction () {
    this.state.message = ''
  }
}

store 的 state 來存數據,store 裏面有一堆的 action,這些 action 來控制 state 的改變,也就是不直接去對 state 做改變,而是通過 action 來改變,因爲都走 action,我們就可以知道到底改變(mutation)是如何被觸發的,出現錯誤,也可以記錄記錄日誌啥的。

image.png | center | 519x657

不過這裏沒有限制組件裏面不能修改 store 裏面的 state,萬一組件瞎胡修改,不通過 action,那我們也沒法跟蹤這些修改是怎麼發生的。所以就需要規定一下,組件不允許直接修改屬於 store 實例的 state,組件必須通過 action 來改變 state,也就是說,組件裏面應該執行 action 來分發 (dispatch) 事件通知 store 去改變。這樣約定的好處是,我們能夠記錄所有 store 中發生的 state 改變,同時實現能做到記錄變更 (mutation)、保存狀態快照、歷史回滾/時光旅行的先進的調試工具。

這樣進化了一下,一個簡單的 Flux 架構就實現了。

Flux

Flux其實是一種思想,就像MVC,MVVM之類的,他給出了一些基本概念,所有的框架都可以根據他的思想來做一些實現。

Flux把一個應用分成了4個部分:

  • View
  • Action
  • Dispatcher
  • Store

image.png | center | 827x250

比如我們搞一個應用,顯而易見,這個應用裏面會有一堆的 View,這個 View 可以是Vue的,也可以是 React的,啥框架都行,啥技術都行。

View 肯定是要展示數據的,所謂的數據,就是 Store,Store 很容易明白,就是存數據的地方。當然我們可以把 Store 都放到一起,也可以分開來放,所以就有一堆的 Store。但是這些 View 都有一個特點,就是 Store 變了得跟着變。

View 怎麼跟着變呢?一般 Store 一旦發生改變,都會往外面發送一個事件,比如 change,通知所有的訂閱者。View 通過訂閱也好,監聽也好,不同的框架有不同的技術,反正 Store 變了,View 就會變。

View 不是光用來看的,一般都會有用戶操作,用戶點個按鈕,改個表單啥的,就需要修改 Store。Flux 要求,View 要想修改 Store,必須經過一套流程,有點像我們剛纔 Store 模式裏面說的那樣。視圖先要告訴 Dispatcher,讓 Dispatcher dispatch 一個 action,Dispatcher 就像是個中轉站,收到 View 發出的 action,然後轉發給 Store。比如新建一個用戶,View 會發出一個叫 addUser 的 action 通過 Dispatcher 來轉發,Dispatcher 會把 addUser 這個 action 發給所有的 store,store 就會觸發 addUser 這個 action,來更新數據。數據一更新,那麼 View 也就跟着更新了。

這個過程有幾個需要注意的點:

  • Dispatcher 的作用是接收所有的 Action,然後發給所有的 Store。這裏的 Action 可能是 View 觸發的,也有可能是其他地方觸發的,比如測試用例。轉發的話也不是轉發給某個 Store,而是所有 Store。
  • Store 的改變只能通過 Action,不能通過其他方式。也就是說 Store 不應該有公開的 Setter,所有 Setter 都應該是私有的,只能有公開的 Getter。具體 Action 的處理邏輯一般放在 Store 裏。

聽聽描述看看圖,可以發現,Flux的最大特點就是數據都是單向流動的。

Redux

Flux 有一些缺點(特點),比如一個應用可以擁有多個 Store,多個Store之間可能有依賴關係;Store 封裝了數據還有處理數據的邏輯。

所以大家在使用的時候,一般會用 Redux,他和 Flux 思想比較類似,也有差別。

image.png | center | 827x380

Store

Redux 裏面只有一個 Store,整個應用的數據都在這個大 Store 裏面。Store 的 State 不能直接修改,每次只能返回一個新的 State。Redux 整了一個 createStore 函數來生成 Store。

import { createStore } from 'redux';
const store = createStore(fn);

Store 允許使用  store.subscribe  方法設置監聽函數,一旦 State 發生變化,就自動執行這個函數。這樣不管 View 是用什麼實現的,只要把 View 的更新函數 subscribe 一下,就可以實現 State 變化之後,View 自動渲染了。比如在 React 裏,把組件的render方法或setState方法訂閱進去就行。

Action

和 Flux  一樣,Redux 裏面也有 Action,Action 就是 View 發出的通知,告訴 Store State 要改變。Action 必須有一個 type 屬性,代表 Action 的名稱,其他可以設置一堆屬性,作爲參數供 State 變更時參考。

const action = {
  type: 'ADD_TODO',
  payload: 'Learn Redux'
};

Redux 可以用 Action Creator 批量來生成一些 Action。

Reducer

Redux 沒有 Dispatcher 的概念,Store 裏面已經集成了 dispatch 方法。store.dispatch()是 View 發出 Action 的唯一方法。

import { createStore } from 'redux';
const store = createStore(fn);

store.dispatch({
  type: 'ADD_TODO',
  payload: 'Learn Redux'
});

Redux 用一個叫做 Reducer 的純函數來處理事件。Store 收到 Action 以後,必須給出一個新的 State(就是剛纔說的Store 的 State 不能直接修改,每次只能返回一個新的 State),這樣 View 纔會發生變化。這種 State 的計算過程就叫做 Reducer。

什麼是純函數呢,就是說沒有任何的副作用,比如這樣一個函數:

function getAge(user) {
  user.age = user.age + 1;
  return user.age;
}

這個函數就有副作用,每一次相同的輸入,都可能導致不同的輸出,而且還會影響輸入 user 的值,再比如:

let b = 10;
function compare(a) {
  return a >= b;
}

這個函數也有副作用,就是依賴外部的環境,b 在別處被改變了,返回值對於相同的 a 就有可能不一樣。

而 Reducer 是一個純函數,對於相同的輸入,永遠都只會有相同的輸出,不會影響外部的變量,也不會被外部變量影響,不得改寫參數。它的作用大概就是這樣,根據應用的狀態和當前的 action 推導出新的 state:

(previousState, action) => newState

類比 Flux,Flux 有些像:

 (state, action) => state

爲什麼叫做 Reducer 呢?reduce  是一個函數式編程的概念,經常和  map  放在一起說,簡單來說,map  就是映射,reduce  就是歸納。映射就是把一個列表按照一定規則映射成另一個列表,而 reduce 是把一個列表通過一定規則進行合併,也可以理解爲對初始值進行一系列的操作,返回一個新的值。

比如  Array 就有一個方法叫 reduce,Array.prototype.reduce(reducer, ?initialValue),把 Array 整吧整吧弄成一個  newValue。

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15

看起來和 Redux 的 Reducer 是不是好像好像,Redux 的 Reducer 就是 reduce 一個列表(action的列表)和一個 initialValue(初始的  State)到一個新的 value(新的  State)。

把上面的概念連起來,舉個例子:

下面的代碼聲明瞭 reducer:

const defaultState = 0;
const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case 'ADD':
      return state + action.payload;
    default: 
      return state;
  }
};

createStore接受 Reducer 作爲參數,生成一個新的 Store。以後每當store.dispatch發送過來一個新的 Action,就會自動調用 Reducer,得到新的 State。

import { createStore } from 'redux';
const store = createStore(reducer);

createStore 內部幹了什麼事兒呢?通過一個簡單的 createStore 的實現,可以瞭解大概的原理(可以略過不看):

const createStore = (reducer) => {
  let state;
  let listeners = [];

  const getState = () => state;

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach(listener => listener());
  };

  const subscribe = (listener) => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter(l => l !== listener);
    }
  };

  dispatch({});

  return { getState, dispatch, subscribe };
};

Redux 有很多的 Reducer,對於大型應用來說,State 必然十分龐大,導致 Reducer 函數也十分龐大,所以需要做拆分。Redux  裏每一個 Reducer 負責維護 State 樹裏面的一部分數據,多個 Reducer 可以通過 combineReducers 方法合成一個根 Reducer,這個根 Reducer 負責維護整個 State。

import { combineReducers } from 'redux';

// 注意這種簡寫形式,State 的屬性名必須與子 Reducer 同名
const chatReducer = combineReducers({
  Reducer1,
  Reducer2,
  Reducer3
})

combineReducers 幹了什麼事兒呢?通過簡單的 combineReducers 的實現,可以瞭解大概的原理(可以略過不看):

const combineReducers = reducers => {
  return (state = {}, action) => {
    return Object.keys(reducers).reduce(
      (nextState, key) => {
        nextState[key] = reducers[key](state[key], action);
        return nextState;
      },
      {} 
    );
  };
};

流程

image.png | center | 827x380

再回顧一下剛纔的流程圖,嘗試走一遍  Redux  流程:

1、用戶通過 View 發出 Action:

store.dispatch(action);

2、然後 Store 自動調用 Reducer,並且傳入兩個參數:當前 State 和收到的 Action。 Reducer 會返回新的 State 。

let nextState = xxxReducer(previousState, action);

3、State 一旦有變化,Store 就會調用監聽函數。

store.subscribe(listener);

4、listener可以通過  store.getState()  得到當前狀態。如果使用的是 React,這時可以觸發重新渲染 View。

function listerner() {
  let newState = store.getState();
  component.setState(newState);   
}

對比 Flux

和  Flux  比較一下:Flux 中 Store 是各自爲戰的,每個 Store 只對對應的 View 負責,每次更新都只通知對應的View:

image.png | left | 827x429

Redux 中各子 Reducer 都是由根 Reducer 統一管理的,每個子 Reducer 的變化都要經過根 Reducer 的整合:

image.png | left | 827x395

簡單來說,Redux有三大原則:

  • 單一數據源:Flux 的數據源可以是多個。
  • State 是隻讀的:Flux 的 State 可以隨便改。
  • 使用純函數來執行修改:Flux 執行修改的不一定是純函數。

Redux 和 Flux 一樣都是單向數據流

中間件

剛纔說到的都是比較理想的同步狀態。在實際項目中,一般都會有同步和異步操作,所以 Flux、Redux 之類的思想,最終都要落地到同步異步的處理中來。

在  Redux  中,同步的表現就是:Action 發出以後,Reducer 立即算出 State。那麼異步的表現就是:Action 發出以後,過一段時間再執行 Reducer。

那怎麼才能 Reducer 在異步操作結束後自動執行呢?Redux 引入了中間件 Middleware 的概念。

其實我們重新回顧一下剛纔的流程,可以發現每一個步驟都很純粹,都不太適合加入異步的操作,比如 Reducer,純函數,肯定不能承擔異步操作,那樣會被外部IO干擾。Action呢,就是一個純對象,放不了操作。那想來想去,只能在 View 裏發送 Action 的時候,加上一些異步操作了。比如下面的代碼,給原來的  dispatch  方法包裹了一層,加上了一些日誌打印的功能:

let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action);
  next(action);
  console.log('next state', store.getState());
}

既然能加日誌打印,當然也能加入異步操作。所以中間件簡單來說,就是對 store.dispatch 方法進行一些改造的函數。不展開說了,所以如果想詳細瞭解中間件,可以點這裏

Redux 提供了一個 applyMiddleware 方法來應用中間件:

const store = createStore(
  reducer,
  applyMiddleware(thunk, promise, logger)
);

這個方法主要就是把所有的中間件組成一個數組,依次執行。也就是說,任何被髮送到 store 的 action 現在都會經過thunk,promise,logger 這幾個中間件了。

處理異步

對於異步操作來說,有兩個非常關鍵的時刻:發起請求的時刻,和接收到響應的時刻(可能成功,也可能失敗或者超時),這兩個時刻都可能會更改應用的 state。一般是這樣一個過程:

  1. 請求開始時,dispatch  一個請求開始 Action,觸發 State 更新爲“正在請求”狀態,View 重新渲染,比如展現個Loading啥的。
  2. 請求結束後,如果成功,dispatch  一個請求成功 Action,隱藏掉  Loading,把新的數據更新到  State;如果失敗,dispatch  一個請求失敗 Action,隱藏掉  Loading,給個失敗提示。

顯然,用  Redux  處理異步,可以自己寫中間件來處理,當然大多數人會選擇一些現成的支持異步處理的中間件。比如 redux-thunk 或 redux-promise 。

Redux-thunk

thunk 比較簡單,沒有做太多的封裝,把大部分自主權交給了用戶:

const createFetchDataAction = function(id) {
    return function(dispatch, getState) {
        // 開始請求,dispatch 一個 FETCH_DATA_START action
        dispatch({
            type: FETCH_DATA_START, 
            payload: id
        })
        api.fetchData(id) 
            .then(response => {
                // 請求成功,dispatch 一個 FETCH_DATA_SUCCESS action
                dispatch({
                    type: FETCH_DATA_SUCCESS,
                    payload: response
                })
            })
            .catch(error => {
                // 請求失敗,dispatch 一個 FETCH_DATA_FAILED action   
                dispatch({
                    type: FETCH_DATA_FAILED,
                    payload: error
                })
            }) 
    }
}

//reducer
const reducer = function(oldState, action) {
    switch(action.type) {
    case FETCH_DATA_START : 
        // 處理 loading 等
    case FETCH_DATA_SUCCESS : 
        // 更新 store 等
    case FETCH_DATA_FAILED : 
        // 提示異常
    }
}

缺點就是用戶要寫的代碼有點多,可以看到上面的代碼比較囉嗦,一個請求就要搞這麼一套東西。

Redux-promise

redus-promise 和 redux-thunk 的思想類似,只不過做了一些簡化,成功失敗手動 dispatch 被封裝成自動了:

const FETCH_DATA = 'FETCH_DATA'
//action creator
const getData = function(id) {
    return {
        type: FETCH_DATA,
        payload: api.fetchData(id) // 直接將 promise 作爲 payload
    }
}
//reducer
const reducer = function(oldState, action) {
    switch(action.type) {
    case FETCH_DATA: 
        if (action.status === 'success') {
             // 更新 store 等處理
        } else {
                // 提示異常
        }
    }
}

剛纔的什麼 then、catch 之類的被中間件自行處理了,代碼簡單不少,不過要處理 Loading 啥的,還需要寫額外的代碼。

其實任何時候都是這樣:封裝少,自由度高,但是代碼就會變複雜;封裝多,代碼變簡單了,但是自由度就會變差。redux-thunk 和 redux-promise 剛好就是代表這兩個面。

redux-thunk 和 redux-promise  的具體使用就不介紹了,這裏只聊一下大概的思路。大部分簡單的異步業務場景,redux-thunk 或者 redux-promise 都可以滿足了。


上面說的 Flux 和 Redux,和具體的前端框架沒有什麼關係,只是思想和約定層面。下面就要和我們常用的 Vue 或 React 結合起來了:

Vuex

Vuex 主要用於 Vue,和 Flux,Redux 的思想很類似。

image.png | center | 701x551

Store

每一個 Vuex 裏面有一個全局的 Store,包含着應用中的狀態 State,這個 State 只是需要在組件中共享的數據,不用放所有的 State,沒必要。這個 State 是單一的,和 Redux 類似,所以,一個應用僅會包含一個 Store 實例。單一狀態樹的好處是能夠直接地定位任一特定的狀態片段,在調試的過程中也能輕易地取得整個當前應用狀態的快照。

Vuex通過 store 選項,把 state 注入到了整個應用中,這樣子組件能通過 this.$store 訪問到 state 了。

const app = new Vue({
  el: '#app',
  // 把 store 對象提供給 “store” 選項,這可以把 store 的實例注入所有的子組件
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
})
const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return this.$store.state.count
    }
  }
}

State 改變,View 就會跟着改變,這個改變利用的是 Vue 的響應式機制。

Mutation

顯而易見,State 不能直接改,需要通過一個約定的方式,這個方式在 Vuex 裏面叫做 mutation,更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation。Vuex 中的 mutation 非常類似於事件:每個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)。

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 變更狀態
      state.count++
    }
  }
})

觸發 mutation 事件的方式不是直接調用,比如 increment(state)  是不行的,而要通過 store.commit 方法:

store.commit('increment')

注意:mutation 都是同步事務

mutation 有些類似 Redux 的 Reducer,但是 Vuex 不要求每次都搞一個新的 State,可以直接修改 State,這塊兒又和 Flux 有些類似。具尤大的說法,Redux 強制的 immutability,在保證了每一次狀態變化都能追蹤的情況下強制的 immutability 帶來的收益很有限,爲了同構而設計的 API 很繁瑣,必須依賴第三方庫才能相對高效率地獲得狀態樹的局部狀態,這些都是 Redux 不足的地方,所以也被 Vuex 舍掉了。

到這裏,其實可以感覺到 Flux、Redux、Vuex 三個的思想都差不多,在具體細節上有一些差異,總的來說都是讓 View 通過某種方式觸發 Store 的事件或方法,Store 的事件或方法對 State 進行修改或返回一個新的 State,State 改變之後,View 發生響應式改變。

Action

到這裏又該處理異步這塊兒了。mutation 是必須同步的,這個很好理解,和之前的  reducer 類似,不同步修改的話,會很難調試,不知道改變什麼時候發生,也很難確定先後順序,A、B兩個 mutation,調用順序可能是 A -> B,但是最終改變 State 的結果可能是 B -> A。

對比Redux的中間件,Vuex 加入了 Action 這個東西來處理異步,Vuex的想法是把同步和異步拆分開,異步操作想咋搞咋搞,但是不要干擾了同步操作。View 通過 store.dispatch('increment') 來觸發某個 Action,Action 裏面不管執行多少異步操作,完事之後都通過 store.commit('increment') 來觸發 mutation,一個 Action 裏面可以觸發多個 mutation。所以 Vuex 的Action 類似於一個靈活好用的中間件。

Vuex 把同步和異步操作通過 mutation 和 Action 來分開處理,是一種方式。但不代表是唯一的方式,還有很多方式,比如就不用 Action,而是在應用內部調用異步請求,請求完畢直接 commit mutation,當然也可以。

Vuex 還引入了 Getter,這個可有可無,只不過是方便計算屬性的複用。

Vuex 單一狀態樹並不影響模塊化,把 State 拆了,最後組合在一起就行。Vuex 引入了 Module 的概念,每個 Module 有自己的 state、mutation、action、getter,其實就是把一個大的 Store 拆開。

總的來看,Vuex 的方式比較清晰,適合 Vue 的思想,在實際開發中也比較方便。

對比Redux

Redux:
view——>actions——>reducer——>state變化——>view變化(同步異步一樣)

Vuex:
view——>commit——>mutations——>state變化——>view變化(同步操作)
view——>dispatch——>actions——>mutations——>state變化——>view變化(異步操作)

React-redux

Redux 和 Flux 類似,只是一種思想或者規範,它和 React 之間沒有關係。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。

但是因爲 React 包含函數式的思想,也是單向數據流,和 Redux 很搭,所以一般都用  Redux 來進行狀態管理。爲了簡單處理  Redux  和 React  UI  的綁定,一般通過一個叫 react-redux 的庫和 React 配合使用,這個是  react  官方出的(如果不用 react-redux,那麼手動處理 Redux 和 UI 的綁定,需要寫很多重複的代碼,很容易出錯,而且有很多 UI 渲染邏輯的優化不一定能處理好)。

Redux將React組件分爲容器型組件和展示型組件,容器型組件一般通過connect函數生成,它訂閱了全局狀態的變化,通過mapStateToProps函數,可以對全局狀態進行過濾,而展示型組件不直接從global state獲取數據,其數據來源於父組件。

image.png | center | 827x268

如果一個組件既需要UI呈現,又需要業務邏輯處理,那就得拆,拆成一個容器組件包着一個展示組件。

因爲 react-redux 只是 redux 和 react 結合的一種實現,除了剛纔說的組件拆分,並沒有什麼新奇的東西,所以只拿一個簡單TODO項目的部分代碼來舉例:

入口文件 index.js,把 redux 的相關 store、reducer 通過 Provider 註冊到 App 裏面,這樣子組件就可以拿到  store  了。

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import rootReducer from './reducers'
import App from './components/App'

const store = createStore(rootReducer)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

actions/index.js,創建 Action:

let nextTodoId = 0
export const addTodo = text => ({
  type: 'ADD_TODO',
  id: nextTodoId++,
  text
})

export const setVisibilityFilter = filter => ({
  type: 'SET_VISIBILITY_FILTER',
  filter
})

export const toggleTodo = id => ({
  type: 'TOGGLE_TODO',
  id
})

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

reducers/todos.js,創建 Reducers:

const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ]
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      )
    default:
      return state
  }
}

export default todos

reducers/index.js,把所有的 Reducers 綁定到一起:

import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'

export default combineReducers({
  todos,
  visibilityFilter,
  ...
})

containers/VisibleTodoList.js,容器組件,connect 負責連接React組件和Redux Store:

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
    case 'SHOW_ALL':
    default:
      return todos
  }
}

// mapStateToProps 函數指定如何把當前 Redux store state 映射到展示組件的 props 中
const mapStateToProps = state => ({
  todos: getVisibleTodos(state.todos, state.visibilityFilter)
})

// mapDispatchToProps 方法接收 dispatch() 方法並返回期望注入到展示組件的 props 中的回調方法。
const mapDispatchToProps = dispatch => ({
  toggleTodo: id => dispatch(toggleTodo(id))
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

簡單來說,react-redux 就是多了個 connect 方法連接容器組件和UI組件,這裏的“連接”就是一種映射:

  • mapStateToProps  把容器組件的 state 映射到UI組件的 props
  • mapDispatchToProps 把UI組件的事件映射到 dispatch 方法

Redux-saga

剛纔介紹了兩個Redux 處理異步的中間件 redux-thunk 和 redux-promise,當然 redux 的異步中間件還有很多,他們可以處理大部分場景,這些中間件的思想基本上都是把異步請求部分放在了  action  creator  中,理解起來比較簡單。

redux-saga 採用了另外一種思路,它沒有把異步操作放在 action creator 中,也沒有去處理 reductor,而是把所有的異步操作看成“線程”,可以通過普通的action去觸發它,當操作完成時也會觸發action作爲輸出。saga 的意思本來就是一連串的事件。

redux-saga 把異步獲取數據這類的操作都叫做副作用(Side  Effect),它的目標就是把這些副作用管理好,讓他們執行更高效,測試更簡單,在處理故障時更容易。

在聊 redux-saga 之前,需要熟悉一些預備知識,那就是 ES6 的 Generator

如果從沒接觸過 Generator 的話,看着下面的代碼,給你個1分鐘傻瓜式速成,函數加個星號就是 Generator 函數了,Generator 就是個罵街生成器,Generator 函數裏可以寫一堆 yield 關鍵字,可以記成“丫的”,Generator 函數執行的時候,啥都不幹,就等着調用 next 方法,按照順序把標記爲“丫的”的地方一個一個拎出來罵(遍歷執行),罵到最後沒有“丫的”標記了,就返回最後的return值,然後標記爲 done: true,也就是罵完了(上面只是幫助初學者記憶,別噴~)。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

hw.next() // 先把 'hello' 拎出來,done: false 代表還沒罵完
// { value: 'hello', done: false } next() 方法有固定的格式,value 是返回值,done 代表是否遍歷結束

hw.next() // 再把 'world' 拎出來,done: false 代表還沒罵完
// { value: 'world', done: false }

hw.next() // 沒有 yield 了,就把最後的 return 'ending' 拎出來,done: true 代表罵完了
// { value: 'ending', done: true }

hw.next() // 沒有 yield,也沒有 return 了,真的罵完了,只能擠出來一個 undefined 了,done: true 代表罵完了
// { value: undefined, done: true }

這樣搞有啥好處呢?我們發現 Generator 函數的很多代碼可以被延緩執行,也就是具備了暫停和記憶的功能:遇到yield表達式,就暫停執行後面的操作,並將緊跟在yield後面的那個表達式的值,作爲返回的對象的value屬性值,等着下一次調用next方法時,再繼續往下執行。用 Generator 來寫異步代碼,大概長這樣:

function* gen(){
  var url = 'https://api.github.com/users/github';
  var jsonData = yield fetch(url);
  console.log(jsonData);
}

var g = gen();
var result = g.next(); 
// 這裏的result是 { value: fetch('https://api.github.com/users/github'), done: true }

// fetch(url) 是一個 Promise,所以需要 then 來執行下一步
result.value.then(function(data){
  return data.json();
}).then(function(data){
  // 獲取到 json data,然後作爲參數調用 next,相當於把 data 傳給了 jsonData,然後執行 console.log(jsonData);
  g.next(data);
});

再回到 redux-saga 來,可以把 saga 想象成開了一個以最快速度不斷地調用 next 方法並嘗試獲取所有 yield 表達式值的線程。舉個例子:

// saga.js
import { take, put } from 'redux-saga/effects'

function* mySaga(){ 
    // 阻塞: take方法就是等待 USER_INTERACTED_WITH_UI_ACTION 這個 action 執行
    yield take(USER_INTERACTED_WITH_UI_ACTION);
    // 阻塞: put方法將同步發起一個 action
    yield put(SHOW_LOADING_ACTION, {isLoading: true});
    // 阻塞: 將等待 FetchFn 結束,等待返回的 Promise
    const data = yield call(FetchFn, 'https://my.server.com/getdata');
    // 阻塞: 將同步發起 action (使用剛纔返回的 Promise.then)
    yield put(SHOW_DATA_ACTION, {data: data});
}

這裏用了好幾個yield,簡單理解,也就是每個 yield 都發起了阻塞,saga 會等待執行結果返回,再執行下一指令。也就是相當於take、put、call、put 這幾個方法的調用變成了同步的,上面的全部完成返回了,纔會執行下面的,類似於 await。

用了 saga,我們就可以很細粒度的控制各個副作用每一部的操作,可以把異步操作和同步發起 action 一起,隨便的排列組合。saga 還提供 takeEvery、takeLatest 之類的輔助函數,來控制是否允許多個異步請求同時執行,尤其是 takeLatest,方便處理由於網絡延遲造成的多次請求數據衝突或混亂的問題。

saga 看起來很複雜,主要原因可能是因爲大家不熟悉 Generator 的語法,還有需要學習一堆新增的 API 。如果拋開這些記憶的東西,改造一下,再來看一下代碼:

function mySaga(){ 
    if (action.type === 'USER_INTERACTED_WITH_UI_ACTION') {
        store.dispatch({ type: 'SHOW_LOADING_ACTION', isLoading: true});
        const data = await Fetch('https://my.server.com/getdata');
        store.dispatch({ type: 'SHOW_DATA_ACTION', data: data});
    }
}

上面的代碼就很清晰了吧,全部都是同步的寫法,無比順暢,當然直接這樣寫是不支持的,所以那些 Generator 語法和API,無非就是做一些適配而已。

saga 還能很方便的並行執行異步任務,或者讓兩個異步任務競爭:

// 並行執行,並等待所有的結果,類似 Promise.all 的行爲
const [users, repos] = yield [
  call(fetch, '/users'),
  call(fetch, '/repos')
]

// 並行執行,哪個先完成返回哪個,剩下的就取消掉了
const {posts, timeout} = yield race({
  posts: call(fetchApi, '/posts'),
  timeout: call(delay, 1000)
})

saga 的每一步都可以做一些斷言(assert)之類的,所以非常方便測試。而且很容易測試到不同的分支。

這裏不討論更多 saga 的細節,大家瞭解 saga 的思想就行,細節請看文檔

對比 Redux-thunk

image.png | center | 827x271

比較一下 redux-thunk 和 redux-saga 的代碼:

image.png | center

image.png | center

和 redux-thunk 等其他異步中間件對比來說,redux-saga 主要有下面幾個特點:

  • 異步數據獲取的相關業務邏輯放在了單獨的 saga.js 中,不再是摻雜在 action.js 或 component.js 中。
  • dispatch 的參數是標準的  action,沒有魔法。
  • saga 代碼採用類似同步的方式書寫,代碼變得更易讀。
  • 代碼異常/請求失敗 都可以直接通過 try/catch 語法直接捕獲處理。
  • 很容易測試,如果是 thunk 的 Promise,測試的話就需要不停的 mock 不同的數據。

其實 redux-saga 是用一些學習的複雜度,換來了代碼的高可維護性,還是很值得在項目中使用的。

Dva

Dva是什麼呢?官方的定義是:dva 首先是一個基於 redux 和 redux-saga 的數據流方案,然後爲了簡化開發體驗,dva 還額外內置了 react-router 和 fetch,所以也可以理解爲一個輕量級的應用框架。

簡單理解,就是讓使用 react-redux 和 redux-saga 編寫的代碼組織起來更合理,維護起來更方便。

之前我們聊了 redux、react-redux、redux-saga 之類的概念,大家肯定覺得頭昏腦漲的,什麼 action、reducer、saga 之類的,寫一個功能要在這些js文件裏面不停的切換。

dva 做的事情很簡單,就是讓這些東西可以寫到一起,不用分開來寫了。比如:

app.model({
  // namespace - 對應 reducer 在 combine 到 rootReducer 時的 key 值
  namespace: 'products',
  // state - 對應 reducer 的 initialState
  state: {
    list: [],
    loading: false,
  },
  // subscription - 在 dom ready 後執行
  subscriptions: [
    function(dispatch) {
      dispatch({type: 'products/query'});
    },
  ],
  // effects - 對應 saga,並簡化了使用
  effects: {
    ['products/query']: function*() {
      yield call(delay(800));
      yield put({
        type: 'products/query/success',
        payload: ['ant-tool', 'roof'],
      });
    },
  },
  // reducers - 就是傳統的 reducers
  reducers: {
    ['products/query'](state) {
      return { ...state, loading: true, };
    },
    ['products/query/success'](state, { payload }) {
      return { ...state, loading: false, list: payload };
    },
  },
});

以前書寫的方式是創建  sagas/products.js, reducers/products.js 和 actions/products.js,然後把 saga、action、reducer 啥的分開來寫,來回切換,現在寫在一起就方便多了。

比如傳統的 TODO 應用,用 redux + redux-saga 來表示結構,就是這樣:

image.png | center | 827x558

saga 攔截 add 這個 action, 發起 http 請求, 如果請求成功, 則繼續向 reducer 發一個 addTodoSuccess 的 action, 提示創建成功, 反之則發送 addTodoFail 的 action 即可。

如果使用 Dva,那麼結構圖如下:

image.png | center | 827x542

整個結構變化不大,最主要的就是把 store 及 saga 統一爲一個 model 的概念(有點類似 Vuex 的 Module),寫在了一個 js 文件裏。增加了一個 Subscriptions, 用於收集其他來源的 action,比如快捷鍵操作。

app.model({
  namespace: 'count',
  state: {
    record: 0,
    current: 0,
  },
  reducers: {
    add(state) {
      const newCurrent = state.current + 1;
      return { ...state,
        record: newCurrent > state.record ? newCurrent : state.record,
        current: newCurrent,
      };
    },
    minus(state) {
      return { ...state, current: state.current - 1};
    },
  },
  effects: {
    *add(action, { call, put }) {
      yield call(delay, 1000);
      yield put({ type: 'minus' });
    },
  },
  subscriptions: {
    keyboardWatcher({ dispatch }) {
      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
    },
  },
});

之前我們說過約定優於配置的思想,Dva正式借鑑了這個思想。

MobX

前面扯了這麼多,其實還都是 Flux 體系的,都是單向數據流方案。接下來要說的 MobX,就和他們不太一樣了。

我們先清空一下大腦,回到初心,什麼是初心?就是我們最初要解決的問題是什麼?最初我們其實爲了解決應用狀態管理的問題,不管是 Redux 還是 MobX,把狀態管理好是前提。什麼叫把狀態管理好,簡單來說就是:統一維護公共的應用狀態,以統一併且可控的方式更新狀態,狀態更新後,View跟着更新。不管是什麼思想,達成這個目標就ok。

Flux 體系的狀態管理方式,只是一個選項,但並不代表是唯一的選項。MobX 就是另一個選項。

MobX背後的哲學很簡單:任何源自應用狀態的東西都應該自動地獲得。譯成人話就是狀態只要一變,其他用到狀態的地方就都跟着自動變。

image.png | center

看這篇文章的人,大概率會對面向對象的思想比較熟悉,而對函數式編程的思想略陌生。Flux 或者說 Redux 的思想主要就是函數式編程(FP)的思想,所以學習起來會覺得累一些。而 MobX 更接近於面向對象編程,它把 state 包裝成可觀察的對象,這個對象會驅動各種改變。什麼是可觀察?就是 MobX 老大哥在看着 state 呢。state 只要一改變,所有用到它的地方就都跟着改變了。這樣整個 View 可以被 state 來驅動。

const obj = observable({
    a: 1,
    b: 2
})

autoRun(() => {
    console.log(obj.a)
})

obj.b = 3 // 什麼都沒有發生
obj.a = 2 // observe 函數的回調觸發了,控制檯輸出:2

上面的obj,他的 obj.a 屬性被使用了,那麼只要 obj.a 屬性一變,所有使用的地方都會被調用。autoRun 就是這個老大哥,他看着所有依賴 obj.a 的地方,也就是收集所有對 obj.a 的依賴。當 obj.a 改變時,老大哥就會觸發所有依賴去更新。

MobX 允許有多個 store,而且這些 store 裏的 state 可以直接修改,不用像 Redux 那樣每次還返回個新的。這個有點像 Vuex,自由度更高,寫的代碼更少。不過它也會讓代碼不好維護。

MobX 和 Flux、Redux 一樣,都是和具體的前端框架無關的,也就是說可以用於 React(mobx-react) 或者 Vue(mobx-vue)。一般來說,用到 React 比較常見,很少用於 Vue,因爲 Vuex 本身就類似 MobX,很靈活。如果我們把 MobX 用於 React  或者  Vue,可以看到很多 setState() 和 this.state.xxx = 這樣的處理都可以省了。

還是和上面一樣,只介紹思想。具體 MobX 的使用,可以看這裏

對比 Redux

我們直觀地上兩坨實現計數器代碼:

0febb7ccd7aa4fc6838748948fd1299e.gif | center

Redux:

import React, { Component } from 'react';
import {
  createStore,
  bindActionCreators,
} from 'redux';
import { Provider, connect } from 'react-redux';

// ①action types
const COUNTER_ADD = 'counter_add';
const COUNTER_DEC = 'counter_dec';

const initialState = {a: 0};
// ②reducers
function reducers(state = initialState, action) {
  switch (action.type) {
  case COUNTER_ADD:
    return {...state, a: state.a+1};
  case COUNTER_DEC:
    return {...state, a: state.a-1};
  default:
    return state
  }
}

// ③action creator
const incA = () => ({ type: COUNTER_ADD });
const decA = () => ({ type: COUNTER_DEC });
const Actions = {incA, decA};

class Demo extends Component {
  render() {
    const { store, actions } = this.props;
    return (
      <div>
        <p>a = {store.a}</p>
        <p>
          <button className="ui-btn" onClick={actions.incA}>增加 a</button>
          <button className="ui-btn" onClick={actions.decA}>減少 a</button>
        </p>
      </div>
    );
  }
}

// ④將state、actions 映射到組件 props
const mapStateToProps = state => ({store: state});
const mapDispatchToProps = dispatch => ({
  // ⑤bindActionCreators 簡化 dispatch
  actions: bindActionCreators(Actions, dispatch)
})
// ⑥connect產生容器組件
const Root = connect(
  mapStateToProps,
  mapDispatchToProps
)(Demo)

const store = createStore(reducers)
export default class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <Root />
      </Provider>
    )
  }
}

MobX:

import React, { Component } from 'react';
import { observable, action } from 'mobx';
import { Provider, observer, inject } from 'mobx-react';

// 定義數據結構
class Store {
  // ① 使用 observable decorator 
  @observable a = 0;
}

// 定義對數據的操作
class Actions {
  constructor({store}) {
    this.store = store;
  }
  // ② 使用 action decorator 
  @action
  incA = () => {
    this.store.a++;
  }
  @action
  decA = () => {
    this.store.a--;
  }
}

// ③實例化單一數據源
const store = new Store();
// ④實例化 actions,並且和 store 進行關聯
const actions = new Actions({store});

// inject 向業務組件注入 store,actions,和 Provider 配合使用
// ⑤ 使用 inject decorator 和 observer decorator
@inject('store', 'actions')
@observer
class Demo extends Component {
  render() {
    const { store, actions } = this.props;
    return (
      <div>
        <p>a = {store.a}</p>
        <p>
          <button className="ui-btn" onClick={actions.incA}>增加 a</button>
          <button className="ui-btn" onClick={actions.decA}>減少 a</button>
        </p>
      </div>
    );
  }
}

class App extends Component {
  render() {
    // ⑥使用Provider 在被 inject 的子組件裏,可以通過 props.store props.actions 訪問
    return (
      <Provider store={store} actions={actions}>
        <Demo />
      </Provider>
    )
  }
}

export default App;

比較一下:

  • Redux 數據流流動很自然,可以充分利用時間回溯的特徵,增強業務的可預測性;MobX 沒有那麼自然的數據流動,也沒有時間回溯的能力,但是 View 更新很精確,粒度控制很細。
  • Redux 通過引入一些中間件來處理副作用;MobX  沒有中間件,副作用的處理比較自由,比如依靠 autorunAsync 之類的方法。
  • Redux 的樣板代碼更多,看起來就像是我們要做頓飯,需要先買個調料盒裝調料,再買個架子放刀叉。。。做一大堆準備工作,然後纔開始炒菜;而 MobX 基本沒啥多餘代碼,直接硬來,拿着炊具調料就開幹,搞出來爲止。

但其實 Redux 和 MobX 並沒有孰優孰劣,Redux 比 Mobx 更多的樣板代碼,是因爲特定的設計約束。如果項目比較小的話,使用 MobX 會比較靈活,但是大型項目,像 MobX 這樣沒有約束,沒有最佳實踐的方式,會造成代碼很難維護,各有利弊。一般來說,小項目建議 MobX 就夠了,大項目還是用 Redux 比較合適。

總結

時光荏苒,歲月如梭。每一個框架或者庫只能陪你走一段路,最終都會逝去。留在你心中的,不是一條一條的語法規則,而是一個一個的思想,這些思想纔是推動進步的源泉。

帥哥美女,如果你都看到這裏了,那麼不點個贊,你的良心過得去麼?

參考鏈接

https://cn.vuejs.org/v2/guide/state-management.html
https://vuex.vuejs.org/
https://cn.redux.js.org/docs/react-redux/
http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html
http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html
https://redux-saga-in-chinese.js.org
https://juejin.im/post/59e6cd68f265da43163c2821
https://react-redux.js.org/introduction/why-use-react-redux
https://segmentfault.com/a/1190000007248878
http://es6.ruanyifeng.com/#docs/generator
https://juejin.im/post/5ac1cb9d6fb9a028cf32a046
https://zhuanlan.zhihu.com/p/35437092
https://github.com/dvajs/dva/issues/1
https://cn.mobx.js.org
https://zhuanlan.zhihu.com/p/25585910
http://imweb.io/topic/59f4833db72024f03c7f49b4

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章