React資料狀態管理 --- Redux,Redux-Saga以及進階Dva

炸雞超人發表於2021-10-16

Redux

Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理。除了和 React 一起用外,還支援其它介面庫。 它體小精悍(只有2kB,包括依賴)。

三大原則

單一資料來源

整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中。

console.log(store.getState())

/* 輸出
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}
*/

State 是隻讀的

唯一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通物件。

store.dispatch({
  type: 'COMPLETE_TODO',
  index: 1
})

使用純函式來執行修改

為了描述 action 如何改變 state tree ,你需要編寫 reducers。

function visibilityFilter(state = 'SHOW_ALL', action) {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case 'COMPLETE_TODO':
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: true
          })
        }
        return todo
      })
    default:
      return state
  }
}

import { combineReducers, createStore } from 'redux'
let reducer = combineReducers({ visibilityFilter, todos })
let store = createStore(reducer)

Action

Action 是把資料從應用傳到 store 的有效載荷。它是 store 資料的唯一來源。一般來說你會通過 store.dispatch() 將 action 傳到 store

Action 本質上是 JavaScript 普通物件。我們約定,action 內必須使用一個字串型別的 type 欄位來表示將要執行的動作

// Action 建立函式
export const ADD_TODO = 'ADD_TODO';
export function addTodo(text) {
  return { type: ADD_TODO, text }
}

// 發起dispatch
dispatch(addTodo(text))

Action 建立函式也可以是非同步非純函式。

Reducer

Reducers 指定了應用狀態的變化如何響應 actions 併傳送到 store 的,記住 actions 只是描述了有事情發生了這一事實,並沒有描述應用如何更新 state。

reducer 就是一個純函式,接收舊的 state 和 action,返回新的 state。

(previousState, action) => newState

永遠不要在 reducer 裡做這些操作:

  • 修改傳入引數;
  • 執行有副作用的操作,如 API 請求和路由跳轉;
  • 呼叫非純函式,如 Date.now()Math.random()

只要傳入引數相同,返回計算得到的下一個 state 就一定相同。沒有特殊情況、沒有副作用,沒有 API 請求、沒有變數修改,單純執行計算。

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}

注意:

  1. 不要直接修改 state,而是返回新物件
  2. 在 default 情況下返回舊的 state。遇到未知的 action 時,一定要返回舊的 state。

Store

Store 有以下職責:

  • 維持應用的 state;
  • 提供 getState() 方法獲取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通過 subscribe(listener) 註冊監聽器;
  • 通過 subscribe(listener) 返回的函式登出監聽器。
import { createStore } from 'redux'
import todoApp from './reducers'

const store = createStore(todoApp)

Middleware

在這類框架中,middleware 是指可以被嵌入在框架接收請求到產生響應過程之中的程式碼。

它提供的是位於 action 被髮起之後,到達 reducer 之前的擴充套件點。 你可以利用 Redux middleware 來進行日誌記錄、建立崩潰報告、呼叫非同步介面或者路由等等。

middleware 最優秀的特性就是可以被鏈式組合。你可以在一個專案中使用多個獨立的第三方 middleware。

const loggerMiddleware = createLogger()

const store = createStore(
  rootReducer,
  applyMiddleware(
    thunkMiddleware, // 允許我們 dispatch() 函式
    loggerMiddleware // 一個很便捷的 middleware,用來列印 action 日誌
  )
)

資料流

嚴格的單向資料流是 Redux 架構的設計核心。這意味著應用中所有的資料都遵循相同的生命週期,遵循下面 4 個步驟:

  1. 呼叫 store.dispatch(action)。
  2. Redux store 呼叫傳入的 reducer 函式。
  3. 根 reducer 應該把多個子 reducer 輸出合併成一個單一的 state 樹。
  4. Redux store 儲存了根 reducer 返回的完整 state 樹。

Side Effects: 非同步網路請求本地讀取 localStorage/Cookie 等外界操作

總結

Redux這種單向資料流的庫有很明顯的優缺點

可預測性

action建立函式 reducer都是純函式

stateaction 是簡單物件

state 可以使用 immutable 持久化資料

整套流程職責非常清晰,資料可追蹤可回溯,能很好保證專案穩定性

可擴充套件性

通過 middleware 定製 action 的處理,通過 reducer enhancer 擴充套件 reducer 等等

管理麻煩

redux 的專案通常要分 reducer, action, saga, component 等等,開發中需要來回切換

redux-saga

redux-saga 是一個用於管理應用程式 Side Effect(副作用,例如非同步獲取資料,訪問瀏覽器快取等)的 library,它的目標是讓副作用管理更容易,執行更高效,測試更簡單,在處理故障時更容易。

redux-saga 使用了 ES6 的 Generator 功能,讓非同步的流程更易於讀取,寫入和測試。

核心術語

Effect

一個 effect 就是一個 Plain Object JavaScript 物件,包含一些將被 saga middleware 執行的指令。

使用 redux-saga 提供的工廠函式來建立 effect。 舉個例子,你可以使用 call(myfunc, 'arg1', 'arg2') 指示 middleware 呼叫 myfunc('arg1', 'arg2') 並將結果返回給 yield effect 的那個 Generator。

Task

一個 task 就像是一個在後臺執行的程式。在基於 redux-saga 的應用程式中,可以同時執行多個 task。通過 fork 函式來建立 task:

function* saga() {
  ...
  const task = yield fork(otherSaga, ...args)
  ...
}

阻塞呼叫/非阻塞呼叫

阻塞呼叫的意思是,Saga 在 yield Effect 之後會等待其執行結果返回,結果返回後才會恢復執行 Generator 中的下一個指令。

非阻塞呼叫的意思是,Saga 會在 yield Effect 之後立即恢復執行。

function* saga() {
  yield take(ACTION)              // 阻塞: 將等待 action
  yield call(ApiFn, ...args)      // 阻塞: 將等待 ApiFn (如果 ApiFn 返回一個 Promise 的話)
  yield call(otherSaga, ...args)  // 阻塞: 將等待 otherSaga 結束

  yield put(...)                   // 阻塞: 將同步發起 action (使用 Promise.then)

  const task = yield fork(otherSaga, ...args)  // 非阻塞: 將不會等待 otherSaga
  yield cancel(task)                           // 非阻塞: 將立即恢復執行
  // or
  yield join(task)                             // 阻塞: 將等待 task 結束
}

Watcher/Worker

指的是一種使用兩個單獨的 Saga 來組織控制流的方式。

  • Watcher: 監聽發起的 action 並在每次接收到 action 時 fork 一個 worker。
  • Worker: 處理 action 並結束它。
function* watcher() {
  while(true) {
    const action = yield take(ACTION)
    yield fork(worker, action.payload)
  }
}

function* worker(payload) {
  // ... do some stuff
}

Saga 輔助函式

redux-saga 提供了一些輔助函式,包裝了一些內部方法,用來在一些特定的 action 被髮起到 Store 時派生任務。

讓我們通過常見的 AJAX 例子來演示一下。每次點選 Fetch 按鈕時,我們發起一個 FETCH_REQUESTED 的 action。 我們想通過啟動一個從伺服器獲取一些資料的任務,來處理這個 action。

首先我們建立一個將執行非同步 action 的任務:

import { call, put } from 'redux-saga/effects'

export function* fetchData(action) {
   try {
      // 發起請求
      const data = yield call(Api.fetchUser, action.payload.url);
      // 建立action
      yield put({type: "FETCH_SUCCEEDED", data});
   } catch (error) {
      yield put({type: "FETCH_FAILED", error});
   }
}

然後在每次 FETCH_REQUESTED action 被髮起時啟動上面的任務。

import { takeEvery } from 'redux-saga'

function* watchFetchData() {
  yield* takeEvery('FETCH_REQUESTED', fetchData)
}

還有很多不同作用的輔助函式

  • takeEvery(pattern, saga, ...args)
  • takeEvery(channel, saga, ...args)
  • takeLatest(pattern, saga, ..args)
  • takeLatest(channel, saga, ..args)
  • takeLeading(pattern, saga, ..args)
  • takeLeading(channel, saga, ..args)
  • throttle(ms, pattern, saga, ..args)

宣告式 Effects

redux-saga 的世界裡,Sagas 都用 Generator 函式實現。我們從 Generator 裡 yield 純 JavaScript 物件以表達 Saga 邏輯。 我們稱呼那些物件為 Effect。Effect 是一個簡單的物件,這個物件包含了一些給 middleware 解釋執行的資訊。 你可以把 Effect 看作是傳送給 middleware 的指令以執行某些操作

舉個例子,假設我們有一個監聽 PRODUCTS_REQUESTED action 的 Saga。每次匹配到 action,它會啟動一個從伺服器上獲取產品列表的任務。

import { takeEvery } from 'redux-saga/effects'
import Api from './path/to/api'

function* watchFetchProducts() {
  yield takeEvery('PRODUCTS_REQUESTED', fetchProducts)
}

function* fetchProducts() {
  const products = yield Api.fetch('/products')
  console.log(products)
}

假設我們想測試上面的 generator:

const iterator = fetchProducts()
assert.deepEqual(iterator.next().value, ??) // 我們期望得到什麼?

我們想要檢查 generator yield 的結果的第一個值。在我們的情況裡,這個值是執行 Api.fetch('/products') 這個 Promise 的結果。 在測試過程中,執行真正的服務(real service)是一個既不可行也不實用的方法,所以我們必須 模擬(mock) Api.fetch 函式。 也就是說,我們需要將真實的函式替換為一個假的,這個假的函式並不會真的傳送 AJAX 請求而只會檢查是否用正確的引數呼叫了 Api.fetch

實際上我們需要的只是保證 fetchProducts 任務 yield 一個呼叫正確的函式,並且函式有著正確的引數。

相比於在 Generator 中直接呼叫非同步函式,我們可以僅僅 yield 一條描述函式呼叫的資訊。也就是說,我們將簡單地 yield 一個看起來像下面這樣的物件:

// Effect -> 呼叫 Api.fetch 函式並傳遞 `./products` 作為引數
{
  CALL: {
    fn: Api.fetch,
    args: ['./products']  
  }
}

這樣的話,在測試 Generator 時,所有我們需要做的就是,將 yield 後的物件作一個簡單的 deepEqual 來檢查它是否 yield 了我們期望的指令

出於這樣的原因,redux-saga 提供了一個不一樣的方式來執行非同步呼叫。

import { call } from 'redux-saga/effects'

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
  // ...
}

現在我們不立即執行非同步呼叫,相反,call 建立了一條描述結果的資訊。就像在 Redux 裡你使用 action 建立器,建立一個將被 Store 執行的、描述 action 的純文字物件。 call 建立一個純文字物件描述函式呼叫。redux-saga middleware 確保執行函式呼叫並在響應被 resolve 時恢復 generator。

這讓你能容易地測試 Generator,就算它在 Redux 環境之外。因為 call 只是一個返回純文字物件的函式而已。

import { call } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// expects a call instruction
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)

還有很多不同作用的輔助函式Effect 建立器

  • take(pattern)
  • take.maybe(pattern)
  • take(channel)
  • take.maybe(channel)
  • put(action)
  • put.resolve(action)
  • put(channel, action)
  • call(fn, ...args)
  • call([context, fn], ...args)
  • call([context, fnName], ...args)
  • apply(context, fn, args)
  • cps(fn, ...args)
  • cps([context, fn], ...args)
  • fork(fn, ...args)
  • fork([context, fn], ...args)
  • spawn(fn, ...args)
  • spawn([context, fn], ...args)
  • join(task)
  • join(...tasks)
  • cancel(task)
  • cancel(...tasks)
  • cancel()
  • select(selector, ...args)
  • actionChannel(pattern, [buffer])
  • flush(channel)
  • cancelled()
  • setContext(props)
  • getContext(prop)

Dispatch Actions

假設每次儲存之後,我們想發起一些 action 通知 Store 資料獲取成功了

//...

function* fetchProducts(dispatch)
  const products = yield call(Api.fetch, '/products')
  dispatch({ type: 'PRODUCTS_RECEIVED', products })
}

與我們在上一節中看到的從 Generator 內部直接呼叫函式,有著相同的缺點。如果我們想要測試 fetchProducts 接收到 AJAX 響應之後執行 dispatch, 我們還需要模擬 dispatch 函式。

我們需要同樣的宣告式的解決方案。只需建立一個物件來指示 middleware 我們需要發起一些 action,然後讓 middleware 執行真實的 dispatch。 這種方式我們就可以同樣的方式測試 Generator 的 dispatch:只需檢查 yield 後的 Effect,並確保它包含正確的指令。

redux-saga 為此提供了另外一個函式 put,這個函式用於建立 dispatch Effect。

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

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
  // 建立並 yield 一個 dispatch Effect
  yield put({ type: 'PRODUCTS_RECEIVED', products })
}

現在,我們可以像上一節那樣輕易地測試 Generator:

import { call, put } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// 期望一個 call 指令
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)

// 建立一個假的響應物件
const products = {}

// 期望一個 dispatch 指令
assert.deepEqual(
  iterator.next(products).value,
  put({ type: 'PRODUCTS_RECEIVED', products }),
  "fetchProducts should yield an Effect put({ type: 'PRODUCTS_RECEIVED', products })"
)

現在我們通過 Generator 的 next 方法來將假的響應物件傳遞到 Generator。在 middleware 環境之外, 我們可完全控制 Generator,通過簡單地模擬結果並還原 Generator,我們可以模擬一個真實的環境。 相比於去模擬函式和窺探呼叫(spying calls),模擬資料要簡單的多。

錯誤處理

我們假設遠端讀取因為某些原因失敗了,API 函式 Api.fetch 返回一個被拒絕(rejected)的 Promise。

我們希望通過在 Saga 中發起 PRODUCTS_REQUEST_FAILED action 到 Store 來處理那些錯誤。

import Api from './path/to/api'
import { call, put } from 'redux-saga/effects'

// ...

function* fetchProducts() {
  try {
    const products = yield call(Api.fetch, '/products')
    yield put({ type: 'PRODUCTS_RECEIVED', products })
  }
  catch(error) {
    yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
  }
}

為了測試故障案例,我們將使用 Generator 的 throw 方法。

import { call, put } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// 期望一個 call 指令
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)

// 建立一個模擬的 error 物件
const error = {}

// 期望一個 dispatch 指令
assert.deepEqual(
  iterator.throw(error).value,
  put({ type: 'PRODUCTS_REQUEST_FAILED', error }),
  "fetchProducts should yield an Effect put({ type: 'PRODUCTS_REQUEST_FAILED', error })"
)

我們傳遞一個模擬的 error 物件給 throw,這會引發 Generator 中斷當前的執行流並執行捕獲區塊(catch block)。

你也可以讓你的 API 服務返回一個正常的含有錯誤標識的值。例如, 你可以捕捉 Promise 的拒絕操作,並將它們對映到一個錯誤欄位物件。

import Api from './path/to/api'
import { call, put } from 'redux-saga/effects'

function fetchProductsApi() {
  return Api.fetch('/products')
    .then(response => ({ response }))
    .catch(error => ({ error }))
}

function* fetchProducts() {
  const { response, error } = yield call(fetchProductsApi)
  if (response)
    yield put({ type: 'PRODUCTS_RECEIVED', products: response })
  else
    yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
}

一個登入流程例子

import { take, put, call, fork, cancel } from 'redux-saga/effects'
import Api from '...'

function* authorize(user, password) {
  try {
    const token = yield call(Api.authorize, user, password)
    yield put({type: 'LOGIN_SUCCESS', token})
    yield call(Api.storeItem, {token})
    return token
  } catch(error) {
    yield put({type: 'LOGIN_ERROR', error})
  } finally {
      // finally 區塊執行在任何型別的完成上(正常的 return, 錯誤, 或強制取消), 返回該 generator 是否已經被取消
    if (yield cancelled()) {
      // ... put special cancellation handling code here
    }
  }
}

function* loginFlow() {
  while(true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    // fork return a Task object
    const task = yield fork(authorize, user, password)
    const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
    if(action.type === 'LOGOUT') yield cancel(task)
    yield call(Api.clearItem('token'))
  }
}

loginFlow

  1. 監聽 LOGIN_REQUEST 等待發起action
  2. 從外部獲得引數,以 非阻塞呼叫 的形式執行請求
  3. 監聽 LOGOUTLOGIN_ERROR 等待發起
  4. 如果屬於 LOGOUT 則取消上面請求
  5. 兩種發起都會執行清理流程

authorize

呼叫authorize請求

  • 成功

    1. 發起LOGIN_SUCCESS儲存資料
    2. 執行Api.storeItem
    3. 返回token
  • 錯誤: 發起LOGIN_ERROR
  • 增加取消邏輯

總結

  • 功能強大,多種輔助函式和API,通過這些可以把所有業務邏輯放到saga,優雅而強大,並且保持 Redux 的純粹
  • 可測試性,可以另闢蹊蹺達到功能測試的效果
  • 建立複雜,靈活細粒化的寫法提高編寫和理解門檻

Dva

dva 是基於現有應用架構 (redux + react-router + redux-saga 等)的一層輕量封裝,沒有引入任何新概念,全部程式碼不到 100 行。( Inspired by elm and choo. )

Model

他最核心的是提供了 app.model 方法,用於把 reducer, initialState, action, saga 封裝到一起

比如:

app.model({
  namespace: 'products',
  state: {
    list: [],
    loading: false,
  },
  subscriptions: [
    function(dispatch) {
      dispatch({type: 'products/query'});
    },
  ],
  effects: {
    ['products/query']: function*() {
      yield call(delay(800));
      yield put({
        type: 'products/query/success',
        payload: ['ant-tool', 'roof'],
      });
    },
  },
  reducers: {
    ['products/query'](state) {
      return { ...state, loading: true, };
    },
    ['products/query/success'](state, { payload }) {
      return { ...state, loading: false, list: payload };
    },
  },
});

在有 dva 之前,我們通常會建立 sagas/products.js, reducers/products.jsactions/products.js,然後在這些檔案之間來回切換。

資料流向

資料的改變發生通常是通過使用者互動行為或者瀏覽器行為(如路由跳轉等)觸發的,當此類行為會改變資料的時候可以通過 dispatch 發起一個 action,

  • 如果是同步行為會直接通過 Reducers 改變 State
  • 如果是非同步行為(副作用)會先觸發 Effects 然後流向 Reducers 最終改變 State

State

State 表示 Model 的狀態資料,可以是任意型別值

操作的時候每次都要當作不可變資料(immutable data)來對待,保證每次都是全新物件,沒有引用關係,這樣才能保證 State 的獨立性,便於測試和追蹤變化。

Action

Action 是一個普通 javascript 物件,它是改變 State 的唯一途徑。無論是從 UI 事件、網路回撥,還是 WebSocket 等資料來源所獲得的資料,最終都會通過 dispatch 函式呼叫一個 action,從而改變對應的資料。action 必須帶有 type 屬性指明具體的行為,其它欄位可以自定義,如果要發起一個 action 需要使用 dispatch 函式

dispatch({
  type: 'add',
});

dispatch 函式

dispatching function 是一個用於觸發 action 的函式,action 是改變 State 的唯一途徑,但是它只描述了一個行為,而 dipatch 可以看作是觸發這個行為的方式,而 Reducer 則是描述如何改變資料的。

dispatch({
  type: 'user/add', // 如果在 model 外呼叫,需要新增 namespace
  payload: {}, // 需要傳遞的資訊
});

Reducer

$$ type Reducer<S, A> = (state: S, action: A) => S $$

接受兩個引數:之前已經累積運算的結果和當前要被累積的值,返回的是一個新的累積結果。

在 dva 中,reducers 聚合積累的結果是當前 model 的 state 物件。通過 actions 中傳入的值,與當前 reducers 中的值進行運算獲得新的值。需要注意的是 Reducer 必須是純函式,所以同樣的輸入必然得到同樣的輸出,它們不應該產生任何副作用。並且,每一次的計算都應該使用immutable data,這種特性簡單理解就是每次操作都是返回一個全新的資料(獨立,純淨),所以熱過載和時間旅行這些功能才能夠使用。

Effect

Effect 被稱為副作用,之所以叫副作用是因為它使得我們的函式變得不純,同樣的輸入不一定獲得同樣的輸出。

dva 為了控制副作用的操作,底層引入了redux-sagas做非同步流程控制,由於採用了generator的相關概念,所以將非同步轉成同步寫法,從而將effects轉為純函式。

Subscription

$$ ({ dispatch, history }, done) => unlistenFunction $$

Subscriptions 是一種從 獲取資料的方法,它來自於 elm。在 app.start() 時被執行,資料來源可以是當前的時間、伺服器的 websocket 連線、keyboard 輸入、geolocation 變化、history 路由變化等等。

Subscription 語義是訂閱,用於訂閱一個資料來源,然後根據條件 dispatch 需要的 action。

import key from 'keymaster';
...
app.model({
  namespace: 'count',
  subscriptions: {
    keyEvent({dispatch}) {
      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
    },
  }
});

官網說的比較籠統,實際上它的流程大概如下

  1. key的名稱沒有任何約束,只是用於在儲存,最大作用用來取消監聽
  2. dispatch只能作用當前model所在的reducereffects
  3. 只會在呼叫 app.start() 的時候,遍歷所有 model 中的 subscriptions 執行一遍。
  4. 配置的函式需要返回一個函式,該函式應該用來取消訂閱的該資料來源。呼叫app.unmodel()執行

Dva 圖解

最常見的 Web 類示例之一: TodoList = Todo list + Add todo button

圖解一: React 表示法

按照 React 官方指導意見, 如果多個 Component 之間要發生互動, 那麼狀態(即: 資料)就維護在這些 Component 的最小公約父節點上, 也即是 <App/>

<TodoList/> <Todo/> 以及<AddTodoBtn/> 本身不維持任何 state, 完全由父節點<App/> 傳入 props 以決定其展現, 是一個純函式的存在形式, 即: Pure Component

圖解二: Redux 表示法

React 只負責頁面渲染, 而不負責頁面邏輯, 頁面邏輯可以從中單獨抽取出來, 變成 store

與圖一相比, 幾個明顯的改進點:

  1. 狀態及頁面邏輯從 <App/>裡面抽取出來, 成為獨立的 store, 頁面邏輯就是 reducer
  2. <TodoList/><AddTodoBtn/>都是 Pure Component, 通過 connect 方法可以很方便地給它倆加一層 wrapper 從而建立起與 store 的聯絡: 可以通過 dispatch 向 store 注入 action, 促使 store 的狀態進行變化, 同時又訂閱了 store 的狀態變化, 一旦狀態有變, 被 connect 的元件也隨之重新整理
  3. 使用 dispatch 往 store 傳送 action 的這個過程是可以被攔截的, 自然而然地就可以在這裡增加各種 Middleware, 實現各種自定義功能

這樣一來, 各個部分各司其職, 耦合度更低, 複用度更高, 擴充套件性更好

圖解三: 加入 Saga

  1. 點選建立 Todo 的按鈕, 發起一個 type = addTodo 的 action
  2. saga 攔截這個 action, 發起 http 請求, 如果請求成功, 則繼續向 reducer 發一個 type = addTodoSucc 的 action, 提示建立成功, 反之則傳送 type = addTodoFail 的 action 即可

圖解四: Dva 表示法

Dva 是基於 React + Redux + Saga 的最佳實踐沉澱, 做了 3 件很重要的事情, 大大提升了編碼體驗:

  1. 把 store 及 saga 統一為一個 model 的概念, 寫在一個 js 檔案裡面
  2. 增加了一個 Subscriptions, 用於收集其他來源的 action
  3. model 寫法很簡約, 類似於 DSL 或者 RoR
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'}) });
    },
  },
});

相關文章