天天看點

redux-saga學習筆記

背景:

redux-saga

主要用來處理異步的

actions

action->reducer

的過程變為

action->中間件->reducer

由于團隊内部的

saga

使用不規範,是以輸出一些saga通用使用。

準備:

安裝
$ yarn add react-saga
           
引入
//main.js
import { createStore,applyMiddleware } from 'redux'//applyMiddleware把中間件應用起來,可以放多個
import createSagaMiddleware from 'redux-saga'
import rootSaga from './sagas'
//使用 `redux-saga` 中間件将 Saga 與 Redux Store 建立連接配接。
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  rootReducer,
  applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)
           

有多個

saga

同時啟動,需要進行整合,在

saga.js

子產品中引入

rootSaga

//sagas.js
import { delay } from 'redux-saga'
import { put, takeEvery,all } from 'redux-saga/effects'
export function* helloSaga() {
  console.log('Hello Sagas!');
}
export function* incrementAsync() {
  ...
}

function* watchIncrementAsync() {
	...
}
export default function* rootSaga() {
  yield all([
    helloSaga(),
    watchIncrementAsync()
  ])
}
           

使用:

調用函數(call)

我們從 Generator 裡

yield Effect

以表達 Saga 邏輯。

yield

後如果是一個

promise

,因為

promise

不能進行比較,為了友善測試,在調用函數的時候都寫成

call

的形式。

yield

後的表達式

call(fetch, '/users')

被傳遞給

next

的調用者。

  • 先後執行多個任務
import { call } from 'redux-saga/effects'

const users = yield call(fetch, '/users'),
      repos = yield call(fetch, '/repos')
           
  • 同步執行多個任務

當我們需要

yield

一個包含 effects 的數組, generator 會被阻塞直到所有的 effects 都執行完畢,或者當一個 effect 被拒絕。一旦其中任何一個任務被拒絕,并行的 Effect 将會被拒絕。在這種情況中,所有其他的 Effect 将被自動取消。

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

const [users, repos] = yield [
  call(fetch, '/users'),
  call(fetch, '/repos')
]
           
  • 擷取執行最快的任務

race

Effect 中。所有參與 race 的任務,除了優勝者(譯注:最先完成的任務),其他任務都會被取消。

  1. 可用作逾時處理
import { race, call, put } from 'redux-saga/effects'
import { delay } from 'redux-saga'

function* fetchPostsWithTimeout() {
  //這裡的 posts和timeout 隻有執行快的那個能取得值
  const {posts, timeout} = yield race({
    posts: call(fetchApi, '/posts'),
    timeout: call(delay, 1000)
  })

  if (posts)
    put({type: 'POSTS_RECEIVED', posts})
  else
    put({type: 'TIMEOUT_ERROR'})
}	
           
  1. 自動取消失敗的 Effects
import { race, take, call } from 'redux-saga/effects'

function* backgroundTask() {
  while (true) { ... }
}

function* watchStartBackgroundTask() {
  while (true) {
    yield take('START_BACKGROUND_TASK')
    //當 CANCEL_TASK 被發起,race 将自動取消 backgroundTask,并在 backgroundTask 中抛出取消錯誤。
    yield race({
      task: call(backgroundTask),
      cancel: take('CANCEL_TASK')
    })
  }
}
           
發起action(put)

put

,這個函數用于建立 dispatch Effect。檢查 yield 後的 Effect,并確定它包含正确的指令。

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

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
 // 建立并 yield 一個 dispatch Effect
 //dispatch({ type: 'PRODUCTS_RECEIVED', products })
  yield put({ type: 'PRODUCTS_RECEIVED', products })
}
           
使用 saga 輔助函數
  • takeEvery: 允許多個

    fetchData

    執行個體同時啟動。在每一個

    action

    到來時派生一個新的任務。
  • takeLatest:得到最新那個請求的響應。 如果已經有一個任務在執行的時候啟動另一個

    fetchData

    ,那之前的這個任務會被自動取消。
//sagas.js
import { takeEvery,takeLatest } from 'redux-saga'
function* fetchData(action) { ... }

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

如果你有多個

Saga

監視不同的

action

,可以用多個内置輔助函數建立不同的觀察者

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

// FETCH_USERS
function* fetchUsers(action) { ... }

// CREATE_USER
function* createUser(action) { ... }

// 同時使用它們
export default function* rootSaga() {
  yield takeEvery('FETCH_USERS', fetchUsers)
  yield takeEvery('CREATE_USER', createUser)
}
           
等待action(take)
  1. takeEvery

把監聽的動作換為通配符

*

。在每次 action 被比對時一遍又一遍地被調用(無法控制何時被調用),無法控制何時停止監聽。

import { select, takeEvery } from 'redux-saga/effects'

function* watchAndLog() {
  yield takeEvery('*', function* logger(action) {
    const state = yield select()

    console.log('action', action)
    console.log('state after', state)
  })
}
           
  1. take

take

會暫停

Generator

直到一個比對的

action

被主動拉取。

  • 監聽單個

    action

    take(‘LOGOUT’)
  • 監聽多個并發的

    action

    ,隻要捕獲到多個

    action

    中的一個,就執行take之後的内容。

    take([‘LOGOUT’, ‘LOGIN_ERROR’])

  • 監聽所有的

    action

    take(’*’)
import { select, take } from 'redux-saga/effects'

function* watchAndLog() {
  while (true) {
    const action = yield take('*')
    const state = yield select()

    console.log('action', action)
    console.log('state after', state)
  }
}
           

我們先把上面的代碼

copy

下來

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

// FETCH_USERS
function* fetchUsers(action) { ... }

// CREATE_USER
function* createUser(action) { ... }

// 同時使用它們
export default function* rootSaga() {
  yield takeEvery('FETCH_USERS', fetchUsers)
  yield takeEvery('CREATE_USER', createUser)
}
           

當兩到多個

action

互相之間沒有邏輯關系時,我們可以使用

takeEvery

但是,當

action

之間存在邏輯關系後,使用

takeEvery

就會出現問題。比如登陸和登出。

由于

takeEvery

action

的分開監管降低了可讀性,程式員必須閱讀多個處理函數的

takeEvery

源代碼并建立起它們之間的邏輯關系。

我們可以用

take

把它改成這樣,這樣兩個代碼

export default function* loginFlow() {
  while(true) {
    yield take('LOGIN')
    // ... perform the login logic
    yield take('LOGOUT')
    // ... perform the logout logic
  }
}

export default function* rootSaga() {
  yield* userSaga()
}
           

優點

  1. saga

    主動拉取

    action

    ,可以控制監聽的開始和結束
  2. 用同步的風格描述控制流,提高了可讀性。
無阻塞調用(fork)和取消調用(cancel)

call

調用時會發生阻塞。 當我們不想錯過

call

下面的

take

等待的

action

,想讓異步調用和等待并行發生時,我們可以用

fork

取代

call

fork

被調用時,它會在背景啟動 task 并傳回 task 對象。

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

// ...

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'))
  }
}
           

一旦任務被

fork

,可以使用

yield cancel(task)

來中止任務執行。取消正在運作的任務。

cancel

會導緻被

fork

task

跳進它的

finally

區塊,我們可以在

finally

區塊中進行清理狀态的操作。

finally

區塊中,可使用

yield cancelled()

來檢查 Generator 是否已經被取消。

import { take, call, put, cancelled } 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 {
    if (yield cancelled()) {
      // ... put special cancellation handling code here
    }
  }
}
           

⚠️注意:

yield cancel(task)

不會等待被取消的任務完成(即執行其 catch 區塊)。一旦取消,任務通常應盡快完成它的清理邏輯然後傳回。

錯誤處理
  1. 使用

    try/catch

    文法在 Saga 中捕獲錯誤
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 })
  }
}
           
  1. 捕捉 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 })
}
           
測試
//saga.spec.js
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 })"
)

// 建立一個模拟的 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 })"
)
           

當測試出現分叉的時候,可以調用

clone

方法

import { put, take } from 'redux-saga/effects';
import { cloneableGenerator } from 'redux-saga/utils';

test('doStuffThenChangeColor', assert => {
  const gen = cloneableGenerator(doStuffThenChangeColor)();
  //前面都是一樣的
  gen.next(); // DO_STUFF
  gen.next(); // CHOOSE_NUMBER
	
  
  //判斷奇偶的時候出現了分叉
  assert.test('user choose an even number', a => {
    const clone = gen.clone();
    a.deepEqual(
      clone.next(chooseNumber(2)).value,
      put(changeUI('red')),
      'should change the color to red'
    );

    a.equal(
      clone.next().done,
      true,
      'it should be done'
    );

    a.end();
  });

  assert.test('user choose an odd number', a => {
    const clone = gen.clone();
    a.deepEqual(
      clone.next(chooseNumber(3)).value,
      put(changeUI('blue')),
      'should change the color to blue'
    );

    a.equal(
      clone.next().done,
      true,
      'it should be done'
    );

    a.end();
  });
});

           

繼續閱讀