Saga 优势

保持 action 的纯粹性

action 应该是无副作用的,saga 通过 effects 的方式保持 action 的纯粹性

强大的异步处理

saga 使用 generator 模式工作,并将 effects 分为不同类型交给中间件分别处理,对于复杂异步处理十分方便:

阻塞与无阻塞

  • fork 是无阻塞型
  • call 是阻塞型

effects 并发

  • takeEvery:每次都执行 task,不管之前的 task 是否结束
  • takeLatest:只执行最新的一次 task,如果之前的任务还没结束,则自动取消。

可控的 action flow

  • 同时执行多个 effects: yield [ call(fetch, '/users'), call(fetch, '/repos')]
  • cancel: 取消一个 task
import { SagaCancellationException } from 'redux-saga'
import {  take, put, call, fork, cancel } from 'redux-saga/effects'
import actions from 'somewhere'
import { someApi, delay } from 'somewhere'

function* bgSync() {
    try {
    while(true) {
        yield put({type: 'startQuery'})
        const result = yield call(someApi)
        yield put({type: 'updateState', result})
        yield call(delay, 5000)
    }
    } catch(error) {
        // 或直接使用 `isCancelError(error)`
        if(error instanceof SagaCancellationException)
        yield put(actions.requestFailure('Sync cancelled!'))
    }
}

function* main() {
    while( yield take(START_BACKGROUND_SYNC) ) {
        // 启动后台任务
        const bgSyncTask = yield fork(bgSync)

        // 等待用户的停止操作
        yield take(STOP_BACKGROUND_SYNC)
        // 用户点击了停止,取消后台任务
        // 将抛出一个 SagaCancellationException 错误至被 fork 的 bgSync 任务
        yield cancel(bgSyncTask)
  }
}
  • take: 暂停 generator ,直到匹配到特定的 action (可以理解为监听一个 action),让我们能在一个集中的地方更好地去描述一个非常规的流程。例如,我们的登陆登出流程,只有存在登陆的情况下,才有可能有登出,因此我们可以把登陆和登出放在同一个 saga task 中处理:
    import { isCancelError } from 'redux-saga'
    import { take, call, put } from 'redux-saga/effects'

    function* authorize(user, password) {
      try {
        const token = yield call(Api.authorize, user, password)
        yield put({type: 'LOGIN_SUCCESS', token})
      } catch(error) {
        if(!isCancelError(error))
          yield put({type: 'LOGIN_ERROR', error})
      }
    }

    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'))
      }
    }
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))

function* handleInput(input) {
  // 500ms 防抖动
  yield call(delay, 500)
  ...其他逻辑巴拉巴拉
}

function* watchInput() {
  let task
  while(true) {
    const { input } = yield take('INPUT_CHANGED')
    if(task)
      yield cancel(task)
    task = yield fork(handleInput, input)
  }
}
  • race:多个 effects 竞争,只取最先成功的那个,其他自动取消
    import { race, take, put } from 'redux-saga/effects'

    function* backgroundTask() {
      // 轮询
      while (true) {
          yield call(delay, 1000);
          yield call(api, params);
       }
    }

    function* watchStartBackgroundTask() {
      while(true) {
        yield take('START_BACKGROUND_TASK')
        yield race({
          task: call(backgroundTask),
          cancel: take('CANCEL_TASK')
        })
      }
    }

可测试

saga 将 effects 分多种类型,每一个 effects 都是描述了具体执行信息的对象,比如说我们常用的 put 和 call,就是两种不同的 effects 。put 是发起一个 action 到 store,而 call 则调用给定的函数。

每次我们 yield 一个 put 或者 call,返回的结果都是纯文本的当前 effects 信息。因此,这种将 E、effect 创建和 effect 执行之间分开的做法,使得可以测试 effects。

e.g.

// saga.js
*query() {
    const data = yield call(query);

    yield put({type: 'updateState', data});
}

// test.js
const iterator = query();

assert.deepEqual(
    iterator.next().value,
    call(query),
    "query should yield an Effect call(query)"
)
assert.deepEqual(
    iterator.next().value,
    put({type: 'updateState', data}),
    "query should yield an Effect put({type: 'updateState', data})"
)
assert.deepEqual(
    iterator.next(),
    { done: true, value: undefined },
    'query must be done'
)