redux-saga 初级学习教程
- 1. 见证 saga
- 1.1. 聚合异步操作
- 1.2. 官方初级教程
- 1.2.1. Hello Sagas
- 1.2.1.1. 试试异步调用
- 1.2.2. 一些解释
- 1.2.1. Hello Sagas
- 2. 基本概念
- 2.1. Saga 辅助函数
- 2.1.3. 实现 AJAX 的常见操作
- 2.2. 声明形式的效应接口 (Effects)
- 2.3. 分发 action 到 Store
- 2.4. 异常处理
- 2.1. Saga 辅助函数
先从 redux
的三原则谈起:
- 唯一的状态源:Store
- 唯一触发状态修改的方法: Action
- 唯一转换状态的纯函数: Reducer
三者中可以添加异步操作的地方只有 store.dispatch(action)
这个环节了,这也是中间件编写的位置。那么是否可以借鉴 redux
的唯一原则将异步操作逻辑统一处理?redux-saga
的设计理念正是如此。
1. 见证 saga
1.1. 聚合异步操作
不同于 redux-thunk
和类似功能的 redux-promise
等中间件,redux-saga
可以被看作是后台运行的进程,监听发起的 action ,决定该 action 是进行异步调用,还是触发其他的 action 发送到 Store 。这样的机制使得在设计应用时可以将异步操作逻辑进行统一管理。
1.2. 官方初级教程
克隆官方初级教程
$ git clone https://github.com/redux-saga/redux-saga-beginner-tutorial
$ cd redux-saga-beginner-tutorial
$ npm install
然后运行:
$ npm run start
打开 http://localhost:9966/ ,如果一切正常浏览器中将显示:
点击 Increment
按钮可增加数字, Decrement
按钮减少数字。
1.2.1. Hello Sagas
在根目录下创建 sagas.js
文件,同时输入代码如下:
export function* helloSaga() {
console.log('Hello Sagas!')
}
这里使用了 Generator
,一种异步流程控制机制,它可以用同步代码写出异步逻辑。接下来需要做两件事件:
- 将
redux-saga
中间件接入 Redux Store 。 - 使用中间件运行 Sagas 程序,也就是刚刚编写的函数。
在 main.js
文件中添加:
// ...
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
// ...
import { helloSaga } from './sagas' // 导入 Sagas
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware) // 添加到中间件
)
sagaMiddleware.run(helloSaga) // 运行 Sagas 函数
const action = type => store.dispatch({type})
// 其余代码不变
刷新后在 Console 界面中可以看到 Hello Sagas!
。
1.2.1.1. 试试异步调用
现在添加一个按钮 Increment after 1 second
,每次点击该按钮,数字会延迟 1 秒钟后增加。
首先在 Counter.js
文件中添加 UI 组件:
const Counter = ({ value, onIncrement, onDecrement, onIncrementAsync }) =>
...
{' '}
Clicked: {value} times
将方法添加到父组件 main.js
:
function render() {
ReactDOM.render(
action('INCREMENT')}
onDecrement={() => action('DECREMENT')}
onIncrementAsync={() => action('INCREMENT_ASYNC')} />,
document.getElementById('root')
)
}
注意: 此时 action 依然是 action 纯对象,而不是函数。(与
redux-thunk
不同)
然后添加一个 Saga 实现异步调用,每次发送 INCREMENT_ASYNC
action 都会延迟 1 秒钟增加数字,在 Sagas.js
文件中写入:
import { delay } from 'redux-saga'
import { put, takeEvery, all } from 'redux-saga/effects'
// Saga 作用函数:执行异步任务
export function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}
// Saga 监听函数:每次监听到 ```INCREMENT_ASYNC``` action ,都会触发一个新的异步任务
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
现在需要执行两个处理不同任务的 Sagas,一个是之前的 helloSaga()
,另外一个就是刚刚创建的 watchIncrementAsync()
,将它们统一输出给运行函数:
// 同时执行一个入口的多个 Sagas
export default function* rootSaga() {
yield all([
helloSaga(),
watchIncrementAsync()
])
}
相应的在 main.js
文件中修改:
// ...
import rootSaga from './sagas'
const sagaMiddleware = createSagaMiddleware()
const store = ...
sagaMiddleware.run(rootSaga)
// ...
完工!看看效果。
当然还是需要一些解释的。
1.2.2. 一些解释
通过实例分析可知,redux-saga
模块的工作方式与 Koa
类似。 Sagas 就是一些 Generator 函数,而 redux-saga
的中间件起到的作用与 co
一样。同时 redux-saga
暴露了多种效应接口 (Effect),其中 put
的作用就是指示中间件触发一个 action 。
2. 基本概念
2.1. Saga 辅助函数
在 redux-saga
中的辅助函数提供了打包内部函数并在特定 action 触发时激活任务等功能。它们建立在底层 API 之上,常用的 takeEvery
所提供的功能就和 redux-thunk
非常相似 。
2.1.3. 实现 AJAX 的常见操作
通过点击一个 “取数据” 按钮,触发了 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/effects'
function* watchFetchData() {
yield takeEvery('FETCH_REQUESTED', fetchData)
}
辅助函数 takeEvery
允许获取数据的多个实例并发。也就是说,创建一个新的获取数据任务无需等待之前的任务结束。如果只想获取最新请求的响应,可以使用 takeLatest
辅助函数, 在某时刻,它只允许一个获取数据任务执行。在这种情况下调用一个新的获取数据任务时,如果之前的任务没有完成那么之前的任务将会自动取消。
import { takeLatest } from 'redux-saga'
function* watchFetchData() {
yield* takeLatest('FETCH_REQUESTED', fetchData)
}
对于多个 actions 有多个 Sagas 进行处理,相应的有多个辅助函数:
import { takeEvery } from 'redux-saga'
// FETCH_USERS
function* fetchUsers(action) { ... }
// CREATE_USER
function* createUser(action) { ... }
// use them in parallel
export default function* rootSaga() {
yield takeEvery('FETCH_USERS', fetchUsers)
yield takeEvery('CREATE_USER', createUser)
}
2.2. 声明形式的效应接口 (Effects)
Sagas 本身就是 Generator 函数,函数中返回若干效应对象,中间件对这些效应对象进行操作。可以大体的认为中间件把这些效应对象看成指令进行操作。在 redux-sagaj/effects
模块中提供了多个生成效应对象的接口。
Sagas 中可以生成各种形式的效应对象,其中 Promise 形式较为简单。
先来看一个监听 PRODUCTS_REQUESTED
action 的 Saga:
import { takeEvery } from 'redux-saga/effects'
import Api from './path/to/api'
function* watchFetchProducts() {
yield takeEvery('PRODUCTS_REQUESTED', fetchProducts)
}
function* fetchProducts() {
const products = yield Api.fetch('/products')
console.log(products)
}
上例中在 fetchProducts
Saga 中执行了 Api.fetch('/products')
函数,该函数会立即执行返回一个 Promise。清晰明了,但是对于测试环节这却带来了不便。Promise 是值还是方法?因此在 Saga 中不生成立即执行的异步函数,使用函数调用的描述来替代。
import { call } from 'redux-saga/effects'
function* fetchProducts() {
const products = yield call(Api.fetch, '/products')
// ...
}
效用接口 call
将异步函数转换成 Javascript 纯对象(效用对象),将该效用对象作为指令交给中间件执行,完毕后将结果返回给 Saga 。
// Effect -> call the function Api.fetch with `./products` as argument
{
CALL: {
fn: Api.fetch,
args: ['./products']
}
}
注意:
call
只是转换作用,此时异步函数还没有执行。
在测试环节中只需对比结果是否是所期望的纯对象即可:
import { call } from 'redux-saga/effects'
import Api from '...'
const iterator = fetchProducts()
// expects a call instruction
assert.deepEqual(
iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts should yield an Effect call(Api.fetch, './products')"
)
效用接口 call
同样也适合对象方法:
yield call([obj, obj.method], arg1, arg2, ...) // as if we did obj.method(arg1, arg2 ...)
yield apply(obj, obj.method, [arg1, arg2, ...])
总结:在 redux-saga
中效用对象类似于其他语言中变量,而效用接口 call
等相当于对该对象的声明,效用对象的具体使用则交给了中间件。这种思想即是声明式编程的核心,符合了 React 技术栈的要求。
最后说明一下在 redux-saga
中也提供了 Node 风格的函数转换接口 cps
。
2.3. 分发 action 到 Store
接上例,当获取数据后需要分发相应的 action 到 Store 中,可以简单的利用 dispatch
函数:
// ...
function* fetchProducts(dispatch) {
const products = yield call(Api.fetch, '/products')
dispatch({ type: 'PRODUCTS_RECEIVED', products })
}
这同样带来了测试的麻烦,是否可以继续沿用异步效用对象声明的方法来处理 action ? redux-saga
提供了另外一种效用接口 put
来处理分发 action,以此生成分发效用对象:
import { call, put } from 'redux-saga/effects'
// ...
function* fetchProducts() {
const products = yield call(Api.fetch, '/products')
// create and yield a dispatch Effect
yield put({ type: 'PRODUCTS_RECEIVED', products })
}
2.4. 异常处理
这节我们来处理之前例子的异常,假设 Api.fetch
函数返回的 Promise 的状态是 rejected。只需使用 try/catch
语法将 PRODUCTS_REQUEST_FAILED
action 发送给 Store:
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 })
}
}
也可以返回一个自定义的信息:
import Api from './path/to最后说明一下jj/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 })
}