Redux-Saga解析

427 阅读7分钟

Dva中实现异步effect的原理分析中我们知道,Dva 底层封装了 redux-saga,并采用 redux-saga 实现异步 effect 。那么本章将简单讲解一下 redux-saga 的起源、如何启动 redux-saga 以及 redux-saga 的一些API。

起源

redux 作为状态管理仓库,在我们前端应用中发挥着非常重要的作用,先放一张官方 redux flow图片: image.png 在 redux 的工作流中,由用户派发 action,Store 接收到 action 后自动调用 Reducer 并传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。

我们知道 redux 中数据流是同步的,不支持异步 action 更新或获取数据,但是在实际项目中异步请求数据绝对是高频出现,因此 redux 中间件 middleWare 诞生了。中间件可以在发出 action ,到 reducer 函数接受 action 之间,执行具有副作用的操作。

redux-thunk 和 redux-saga 绝对是目前两个最受欢迎的 redux 中间件。redux-thunk 的主要思想是扩展 action,使得 action 从一个对象变成一个可以处理副作用的函数。而 redux-saga 使用的仍然是普通的 action,redux-saga的中心思想是拦截,拦截发送的 action ,然后进行副作用的处理,自成一套逻辑来控制异步流。

saga如何工作

redux-saga 实现异步 Effects 依赖 es6 generator 特性。saga 通常 yield 一个 Effect(即一个 Plain Object JavaScript 对象),使用 yield 关键词可以暂停函数执行直到 yield 后面的代码执行完毕。

Effect 是包含一些将被 saga middleware 执行指令的对象。我们可以使用 redux-saga 提供的工厂函数(call, fork, put, take等)来创建 Effect。 举个例子,我们可以使用 call(myfunc, 'arg1', 'arg2') 指示 middleware 调用 myfunc('arg1', 'arg2') 并将结果返回给 yield Effect 的那个 Generator。你可以把 Effect 看作是发送给 middleware 的指令以执行某些操作(调用某些异步函数,发起一个 action 到 store,等等)。

sagas 主要包含3个部分,用于联合执行任务:

  • worker saga 处理所有的异步操作,如调用 API,进行异步请求,并且获得返回结果。
  • watcher saga 监听被 dispatch 的 actions,当接收到 action 或者知道其被触发时,调用 worker saga 执行任务。
  • root saga 立即启动 sagas 的唯一入口,一般是在我们的项目文件入口中引入 saga 的中间件并启动 saga 。

通常我们将 worker saga 和 watcher saga 集中写在 saga.js文件中,用于处理所有的副作用,表达了 sagas 的逻辑。

启动 sagas

首先我们创建一个 sagas.js 的文件,然后添加以下代码片段:

export function* helloSaga() {
  console.log('Hello Sagas!');
}

为了运行我们的 Saga,我们需要:

  • 创建一个 Saga middleware 和要运行的 Sagas(比如我们创建的 helloSaga)
  • 将这个 Saga middleware 连接至 Redux store.

接下来,我们在入口文件中添加如下代码:

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import { helloSaga } from './sagas'

const sagaMiddleware = createSagaMiddleware(helloSaga);
const middlewares = [sagaMiddleware];

const store = createStore(reducer, applyMiddleware(...middlewares));
sagaMiddleware.run(helloSaga);

首先我们引入了编写的helloSaga,即上面说的root saga,并通过使用 redux-saga 模块 createSagaMiddleware 创建了一个 Saga middleware。紧接着我们使用 applyMiddleware 将 Saga middleware 连接至 Store。然后使用 sagaMiddleware.run(helloSaga) 运行 Saga。

sagas.js 文件是 saga 的核心,在这里,我们可以编写 watcher saga 用来监听某个特定的或所有的 action,并执行对应的 worker saga(处理异步、业务逻辑等)。接下来,我们介绍 redux-saga 提供的一些 API 来帮助我们更好的处理这些 action 。

Effect 创建器

前面提到了, Effect 是包含指令的文本对象,redux-saga/effects 提供了多种创建 Effect 的方法。

call(fn, ...args)

创建一个 Effect 描述信息,用来命令 middleware 以参数 args 调用函数 fn。

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

上述代码告诉 middleware 去执行 Api.fetch 函数并传入参数'/products' ,同时将返回的结果赋值给 products, 随后发起一个 PRODUCTS_RECEIVED 的 action。这里由于 call 创建的是阻塞的任务,也就是 Generator 只有等 middleware 执行完 Api.fetch('/products' ) ,并返回之后才会继续执行下面的 dispatch 语句。

fork(fn, ...args)

同时,Sagas 也提供了创建非阻塞式任务的方法 —— fork创建一个 Effect 描述信息,用来命令 middleware 以 非阻塞调用 的形式执行 fn。返回一个 Task 对象,可用于取消对应的分支任务。

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

同样上述的逻辑,采用 fork(Api.fetch, '/products') 之后,Generator 不会在等待 fn 返回结果的时候被 middleware 暂停;恰恰相反地,它在 fn 被调用时便会立即恢复执行。当我们不希望某些异步操作阻塞自主流程的时候, fork 比 call 更适用。

take(pattern)

创建一个 Effect 描述信息,用来命令 middleware 在 Store 上等待指定的 action。 在发起与 pattern 匹配的 action 之前,Generator 将暂停。

  • 如果 pattern 为空或者 * 时,那么将匹配所有发起的 action。
  • 如果 pattern 是一个函数,那么将匹配 pattern(action) 为 true 的 action。(例如,take(action => action.entities) 将匹配哪些 entities 字段为真的 action)
  • 如果 pattern 是一个字符串,那么将匹配 action.type === pattern 的 action。
  • 如果它是一个数组,那么数组中的每一项都适用于上述规则,数组中的任意一项被匹配都会捕获对应的 action 。
function* loginFlow() {
  while (true) {
    yield take('LOGIN')
    // ... perform the login logic
    yield take('LOGOUT')
    // ... perform the logout logic
  }
}

比如,我们可以在监听到 'LOGIN' 的 action 时处理一些逻辑,在监听到 'LOGOUT' 的 action 时处理其他的逻辑, 'LOGIN' 'LOGOUT' 总是成对出现。

cancel(task)

创建一个 Effect 描述信息,用来命令 middleware 取消之前的一个分叉任务。

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

上述代码中,我们监听一个发生在未来的 LOGIN_REQUEST 的 action , 在未来某个时刻发起这个 action 时,Generator 恢复执行,拿到 user 和 password 之后,通过 authorize 函数来验证用户是否合法。这里我们使用的是 fork 而不是 call ,是因为我们不希望在验证用户是否合法操作时,漏掉 LOGOUT 或者 LOGIN_REQUEST 的 action,所以这里选择了 非阻塞的 fork, 而不是 阻塞的 call 。同时 fork 返回一个 Task 对象,如果我们监听到LOGOUT 或者 LOGIN_REQUEST 的 action,即用户进行登出操作或登录发生错误,此时我们将取消 验真用户合法的 分支任务。如果在监听到对应的 action 时,分支任务还未执行完毕, cancel 操作将取消该任务;如果此时分支任务已经执行完毕,则 cancel 操作什么都不会执行。

Saga 辅助函数

除了 take 操作外, redux-saga 提供了很多辅助函数帮助我们更好的拦截 action 。

takeEvery(pattern, saga, ...args)

在发起(dispatch)到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。

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

function* fetchUser(action) {
  ...
}

function* watchFetchUser() {
  yield takeEvery('USER_REQUESTED', fetchUser)
}

我们创建了一个简单的任务 fetchUser。我们在每次 USER_REQUESTED action 被发起时,使用 takeEvery 来启动一个新的 fetchUser 任务。

takeEvery 是一个使用 take 和 fork 构建的高级 API。

const takeEvery = (pattern, saga, ...args) => fork(function*() {
  while (true) {
    const action = yield take(pattern)
    yield fork(saga, ...args.concat(action))
  }
})

takeEvery 允许处理并发的 action(即同时触发相同的 action)。

takeLatest(pattern, saga, ...args)

在发起到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga ,并自动取消之前所有已经启动但仍在执行中的 saga 任务。 takeLatest 与 takeEvery 一样,都是使用 take 和 fork 构建的高级 API,但 takeLatest 仅处理最新触发的那个 action。

const takeLatest = (patternOrChannel, saga, ...args) => fork(function*() {
  let lastTask
  while (true) {
    const action = yield take(patternOrChannel)
    if (lastTask) {
      yield cancel(lastTask) // 如果任务已经结束,cancel 则是空操作
    }
    lastTask = yield fork(saga, ...args.concat(action))
  }
})

总结

redux-saga 和 redux-thunk解决的问题一致,但是实现方式有所不同,saga把异步请求的操作全部放在saga文件中,而thunk只是来原来的基础上,对 action 进行一些操作(若 action 是函数,则执行该函数)。redux-saga 与 redux-thunk 相比,提供了更多的 API 、高级的异步控制流以及并发管理等功能,更加适用于大型复杂项目,但同时学习成本也更高。