在Dva中实现异步effect的原理分析中我们知道,Dva 底层封装了 redux-saga,并采用 redux-saga 实现异步 effect 。那么本章将简单讲解一下 redux-saga 的起源、如何启动 redux-saga 以及 redux-saga 的一些API。
起源
redux 作为状态管理仓库,在我们前端应用中发挥着非常重要的作用,先放一张官方 redux flow图片: 在 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 、高级的异步控制流以及并发管理等功能,更加适用于大型复杂项目,但同时学习成本也更高。