本文共 7286 字,大约阅读时间需要 24 分钟。
redux-saga 初级学习教程
先从 redux
的三原则谈起:
redux.png
三者中可以添加异步操作的地方只有 store.dispatch(action)
这个环节了,这也是中间件编写的位置。那么是否可以借鉴 redux
的唯一原则将异步操作逻辑统一处理?redux-saga
的设计理念正是如此。
不同于 redux-thunk
和类似功能的 redux-promise
等中间件,redux-saga
可以被看作是后台运行的进程,监听发起的 action ,决定该 action 是进行异步调用,还是触发其他的 action 发送到 Store 。这样的机制使得在设计应用时可以将异步操作逻辑进行统一管理。
saga.png
$ git clone https://github.com/redux-saga/redux-saga-beginner-tutorial$ cd redux-saga-beginner-tutorial$ npm install
然后运行:
$ npm run start
打开 ,如果一切正常浏览器中将显示:
tutorial_example
点击 Increment
按钮可增加数字, Decrement
按钮减少数字。
在根目录下创建 sagas.js
文件,同时输入代码如下:
export function* helloSaga() { console.log('Hello Sagas!')}
这里使用了 Generator
,一种异步流程控制机制,它可以用同步代码写出异步逻辑。接下来需要做两件事件:
redux-saga
中间件接入 Redux Store 。在 main.js
文件中添加:
// ...import { createStore, applyMiddleware } from 'redux'import createSagaMiddleware from 'redux-saga'// ...import { helloSaga } from './sagas' // 导入 Sagasconst 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()
,将它们统一输出给运行函数:
// 同时执行一个入口的多个 Sagasexport default function* rootSaga() { yield all([ helloSaga(), watchIncrementAsync() ])}
相应的在 main.js
文件中修改:
// ...import rootSaga from './sagas'const sagaMiddleware = createSagaMiddleware()const store = ...sagaMiddleware.run(rootSaga)// ...
完工!看看效果。
当然还是需要一些解释的。
通过实例分析可知,redux-saga
模块的工作方式与 Koa
类似。 Sagas 就是一些 Generator 函数,而 redux-saga
的中间件起到的作用与 co
一样。同时 redux-saga
暴露了多种效应接口 (Effect),其中 put
的作用就是指示中间件触发一个 action 。
在 redux-saga
中的辅助函数提供了打包内部函数并在特定 action 触发时激活任务等功能。它们建立在底层 API 之上,常用的 takeEvery
所提供的功能就和 redux-thunk
非常相似 。
通过点击一个 “取数据” 按钮,触发了 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_USERSfunction* fetchUsers(action) { ... }// CREATE_USERfunction* createUser(action) { ... }// use them in parallelexport default function* rootSaga() { yield takeEvery('FETCH_USERS', fetchUsers) yield takeEvery('CREATE_USER', createUser)}
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 instructionassert.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
。
接上例,当获取数据后需要分发相应的 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 })}
这节我们来处理之前例子的异常,假设 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 })}作者:wlszouc 链接:https://www.jianshu.com/p/f3c7594c4fb4 來源:简书 简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。