Redux-saga框架使用詳解及Demo教程

光強發表於2019-03-03

前面我們講解過redux框架和dva框架的基本使用,因為dva框架中effects模組設計到了redux-saga中的知識點,可能有的同學們會用dva框架,但是對redux-saga又不是很熟悉,今天我們就來簡單的講解下saga框架的主要API和如何配合redux框架使用

redux-saga 官方地址

leonshi.com/redux-saga-…

Demo執行效果圖

  • todoList
gif
  • CounterApp
gif

示例Demo地址

redux-saga-Demo作者還是按照以前的風格,提供了兩個不同的版本,簡單的 CounterApp, 稍複雜的 TodoList

  • CounterApp

github.com/guangqiang-…

  • todoList

github.com/guangqiang-…

什麼是redux-saga

redux-saga 是一個用於管理 Redux 應用非同步操作的中介軟體(又稱非同步 action)。 redux-saga 通過建立 Sagas 將所有的非同步操作邏輯收集在一個地方集中處理,可以用來代替 redux-thunk 中介軟體。

這意味著應用的邏輯會存在兩個地方:

  • Reducers 負責處理 action 的 state 更新

  • Sagas 負責協調那些複雜或非同步的操作

Sagas是通過Generator函式來建立的,如果有不熟悉 Generator函式使用的,請檢視阮老師對Generator的介紹

Sagas 不同於thunks,thunks 是在action被建立時呼叫,而 Sagas只會在應用啟動時呼叫(但初始啟動的 Sagas 可能會動態呼叫其他 Sagas),Sagas 可以被看作是在後臺執行的程式,Sagas 監聽發起的action,然後決定基於這個 action來做什麼:是發起一個非同步呼叫(比如一個 fetch 請求),還是發起其他的action到Store,甚至是呼叫其他的 Sagas

在 redux-saga 的世界裡,所有的任務都通用 yield Effects 來完成(Effect 可以看作是 redux-saga 的任務單元)。Effects 都是簡單的 Javascript 物件,包含了要被 Saga middleware 執行的資訊(打個比方,你可以看到 Redux action其實是一個個包含執行資訊的物件), redux-saga 為各項任務提供了各種Effect建立器,比如呼叫一個非同步函式,發起一個action到Store,啟動一個後臺任務或者等待一個滿足某些條件的未來的 action

redux-saga框架核心API

一、Saga 輔助函式

redux-saga提供了一些輔助函式,用來在一些特定的action 被髮起到Store時派生任務,下面我先來講解兩個輔助函式:takeEverytakeLatest

  • takeEvery

例如:每次點選 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);
      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 函式可以使用下面的寫法替換

function* watchFetchData() {
  
   while(true){
     yield take(`FETCH_REQUESTED`);
     yield fork(fetchData);
   }
}
複製程式碼
  • takeLatest

在上面的例子中,takeEvery 允許多個 fetchData 例項同時啟動,在某個特定時刻,我們可以啟動一個新的 fetchData 任務, 儘管之前還有一個或多個 fetchData 尚未結束

如果我們只想得到最新那個請求的響應(例如,始終顯示最新版本的資料),我們可以使用 takeLatest 輔助函式

import { takeLatest } from `redux-saga`

function* watchFetchData() {
  yield* takeLatest(`FETCH_REQUESTED`, fetchData)
}
複製程式碼

和takeEvery不同,在任何時刻 takeLatest 只允許執行一個 fetchData 任務,並且這個任務是最後被啟動的那個,如果之前已經有一個任務在執行,那之前的這個任務會自動被取消

二、Effect Creators

redux-saga框架提供了很多建立effect的函式,下面我們就來簡單的介紹下開發中最常用的幾種

  • take(pattern)
  • put(action)
  • call(fn, …args)
  • fork(fn, …args)
  • select(selector, …args)

take(pattern)

take函式可以理解為監聽未來的action,它建立了一個命令物件,告訴middleware等待一個特定的action, Generator會暫停,直到一個與pattern匹配的action被髮起,才會繼續執行下面的語句,也就是說,take是一個阻塞的 effect

用法:

function* watchFetchData() {
   while(true) {
   	 // 監聽一個type`FETCH_REQUESTED` 的action的執行,直到等到這個Action被觸發,才會接著執行下面的 yield fork(fetchData)  語句
     yield take(`FETCH_REQUESTED`);
     yield fork(fetchData);
   }
}
複製程式碼

put(action)

put函式是用來傳送action的 effect,你可以簡單的把它理解成為redux框架中的dispatch函式,當put一個action後,reducer中就會計算新的state並返回,注意: put 也是阻塞 effect

用法:

export function* toggleItemFlow() {
	let list = []
	// 傳送一個type`UPDATE_DATA` 的Action,用來更新資料,引數為 `data:list`
	yield put({
      type: actionTypes.UPDATE_DATA,
      data: list
    })
}
複製程式碼

call(fn, …args)

call函式你可以把它簡單的理解為就是可以呼叫其他函式的函式,它命令 middleware 來呼叫fn 函式, args為函式的引數,注意: fn 函式可以是一個 Generator 函式,也可以是一個返回 Promise 的普通函式,call 函式也是阻塞 effect

用法:

export const delay = ms => new Promise(resolve => setTimeout(resolve, ms))

export function* removeItem() {
  try {
  	// 這裡call 函式就呼叫了 delay 函式,delay 函式為一個返回promise 的函式
    return yield call(delay, 500)
  } catch (err) {
    yield put({type: actionTypes.ERROR})
  }
}
複製程式碼

fork(fn, …args)

fork 函式和 call 函式很像,都是用來呼叫其他函式的,但是fork函式是非阻塞函式,也就是說,程式執行完 yield fork(fn, args) 這一行程式碼後,會立即接著執行下一行程式碼語句,而不會等待fn函式返回結果後,在執行下面的語句

用法:

import { fork } from `redux-saga/effects`

export default function* rootSaga() {
  // 下面的四個 Generator 函式會一次執行,不會阻塞執行
  yield fork(addItemFlow)
  yield fork(removeItemFlow)
  yield fork(toggleItemFlow)
  yield fork(modifyItem)
}
複製程式碼

select(selector, …args)

select 函式是用來指示 middleware呼叫提供的選擇器獲取Store上的state資料,你也可以簡單的把它理解為redux框架中獲取store上的 state資料一樣的功能 :store.getState()

用法:

export function* toggleItemFlow() {
	 // 通過 select effect 來獲取 全域性 state上的 `getTodoList` 中的 list
	 let tempList = yield select(state => state.getTodoList.list)
}
複製程式碼

三、createSagaMiddleware()

createSagaMiddleware 函式是用來建立一個 Redux 中介軟體,將 Sagas 與 Redux Store 連結起來

sagas 中的每個函式都必須返回一個 Generator 物件,middleware 會迭代這個 Generator 並執行所有 yield 後的 Effect(Effect 可以看作是 redux-saga 的任務單元)

用法:

import {createStore, applyMiddleware} from `redux`
import createSagaMiddleware from `redux-saga`
import reducers from `./reducers`
import rootSaga from `./rootSaga`

// 建立一個saga中介軟體
const sagaMiddleware = createSagaMiddleware()

// 建立store
const store = createStore(
  reducers,
  將sagaMiddleware 中介軟體傳入到 applyMiddleware 函式中
  applyMiddleware(sagaMiddleware)
)

// 動態執行saga,注意:run函式只能在store建立好之後呼叫
sagaMiddleware.run(rootSaga)

export default store
複製程式碼

四、middleware.run(sagas, …args)

動態執行sagas,用於applyMiddleware階段之後執行sagas

  • sagas: Function: 一個 Generator 函式
  • args: Array: 提供給 saga 的引數 (除了 Store 的 getState 方法)

注意:動態執行saga語句 middleware.run(sagas) 必須要在store建立好之後才能執行,在 store 之前執行,程式會報錯

以CounterApp Demo來看redux-saga具體使用方式

**index.js **

import React from `react`;
import ReactDOM from `react-dom`;
import {createStore, applyMiddleware} from `redux`
import createSagaMiddleware from `redux-saga`

import rootSaga from `./sagas`
import Counter from `./Counter`
import rootReducer from `./reducers`

const sagaMiddleware = createSagaMiddleware()
let middlewares = []
middlewares.push(sagaMiddleware)

const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore)
const store = createStoreWithMiddleware(rootReducer)

sagaMiddleware.run(rootSaga)

const action = type => store.dispatch({ type })

function render() {
  ReactDOM.render(
    <Counter
      value={store.getState()}
      onIncrement={() => action(`INCREMENT`)}
      onDecrement={() => action(`DECREMENT`)}
      onIncrementAsync={() => action(`INCREMENT_ASYNC`)} />,
    document.getElementById(`root`)
  )
}

render()

store.subscribe(render)
複製程式碼

sagas.js

import { put, call, take,fork } from `redux-saga/effects`;
import { takeEvery, takeLatest } from `redux-saga`

export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

function* incrementAsync() {
  // 延遲 1s 在執行 + 1操作
  yield call(delay, 1000);
  yield put({ type: `INCREMENT` });
}

export default function* rootSaga() {
  // while(true){
  //   yield take(`INCREMENT_ASYNC`);
  //   yield fork(incrementAsync);
  // }

  // 下面的寫法與上面的寫法上等效
  yield* takeEvery("INCREMENT_ASYNC", incrementAsync)
}
複製程式碼

reducer.js

export default function counter(state = 0, action) {
  switch (action.type) {
    case `INCREMENT`:
      return state + 1
    case `DECREMENT`:
      return state - 1
    case `INCREMENT_ASYNC`:
      return state
    default:
      return state
  }
}
複製程式碼

從上面的程式碼結構可以看出,redux-saga的使用方式還是比較簡單的,相比較之前的redux框架的CounterApp,多了一個sagas的檔案,reducers檔案還是之前的使用方式

總結

本文所講解的基本上是redux-saga框架在開發中最常使用到的常用API,還有很多不常用API,請參照redux-saga官方文件:leonshi.com/redux-saga-…

如果同學們看到文章這裡,還是對redux-saga框架的基本使用不熟悉,概念模糊,建議看看作者提供的Demo示例

作者建議:同學們可以將作者之前講解的redux框架和redux-saga框架對比來學習理解,這樣更清楚他們之間的區別和聯絡。

福利時間

  • 作者React Native開源專案OneM地址(按照企業開發標準搭建框架完成開發的):github.com/guangqiang-…:歡迎小夥伴們 star
  • 作者簡書主頁:包含60多篇RN開發相關的技術文章www.jianshu.com/u/023338566… 歡迎小夥伴們:多多關注多多點贊
  • 作者React Native QQ技術交流群:620792950 歡迎小夥伴進群交流學習
  • 友情提示:在開發中有遇到RN相關的技術問題,歡迎小夥伴加入交流群(620792950),在群裡提問、互相交流學習。交流群也定期更新最新的RN學習資料給大家,謝謝大家支援!

相關文章