如果redux需要用到 side effect 异步操作,redux-thunk 和 redux-saga 绝对是目前两个最受欢迎的中间件插件。
redux-saga是一个用于管理redux应用异步操作的中间件,redux-saga通过创建sagas将所有异步操作逻辑收集在一个地方集中处理,可以用来代替redux-thunk中间件。
这意味着应用的逻辑会存在两个地方:
React+Redux Cycle(来源:https://www.youtube.com/watch?v=1QI-UE3-0PU)
redux-saga 使用了 ES6 的 Generator
功能,让异步的流程更易于读取,写入和测试。不同于 redux thunk,你不会再遇到回调地狱了,你可以很容易地测试异步流程并保持你的 action 是干净的。(Generator可以通过next查看每一步的调用结果)
主要根据官方案例构建
📌1.克隆教程仓库
git clone https://github.com/redux-saga/redux-saga-beginner-tutorial.git
📌2.安装依赖
cd redux-saga-beginner-tutorial
npm install
此时项目结构是这样的:
📌3.启动应用
npm start
页面效果:
📌1.创建一个 sagas.js 的文件,然后添加以下代码片段:
export function* helloSaga() {
console.log('Hello Sagas!');
}
为了运行我们的 Saga,我们需要:
📌2.修改 main.js:
import "babel-polyfill"
import React from 'react'
import ReactDOM from 'react-dom'
import {
createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from "redux-saga";
import Counter from './Counter'
import reducer from './reducers'
import {
helloSaga } from "./sagas";
// create the saga middleware
const sagaMiddleware=createSagaMiddleware();
// mount it on the Store
const store = createStore(reducer,applyMiddleware(sagaMiddleware))
const action = type => store.dispatch({
type})
function render() {
ReactDOM.render(
<Counter
value={
store.getState()}
onIncrement={
() => action('INCREMENT')}
onDecrement={
() => action('DECREMENT')} />,
document.getElementById('root')
)
}
// then run the saga
sagaMiddleware.run(helloSaga);
// render the application
render();
//subscribe the render
store.subscribe(render)
首先我们引入 ./sagas 模块中的 Saga。然后使用 redux-saga 模块的 createSagaMiddleware
工厂函数来创建一个 Saga middleware。
运行 helloSaga 之前,我们必须使用 applyMiddleware
将 middleware 连接至 Store。然后使用 sagaMiddleware.run(helloSaga)
运行 Saga。
为了模拟现实中的计算,添加另外一个按钮,用于在点击 1 秒后增加计数
📌1.在 UI 组件上 Counter.js 添加一个额外的按钮和一个回调 onIncrementAsync
。
/*eslint-disable no-unused-vars */
import React, {
Component, PropTypes } from "react";
const Counter = ({
value, onIncrement, onDecrement, onIncrementAsync }) => (
<div>
<button onClick={
onIncrement}>Increment</button>{
" "}
<button onClick={
onDecrement}>Decrement</button>
<button onClick={
onIncrementAsync}>Increment after 1 second</button>
<hr />
<div>Clicked: {
value} times</div>
</div>
);
Counter.propTypes = {
value: PropTypes.number.isRequired,
onIncrement: PropTypes.func.isRequired,
onDecrement: PropTypes.func.isRequired,
};
export default Counter;
接下来我们需要将组件的 onIncrementAsync 与 Store action 连接起来。
📌2.修改 main.js 模块:
import "babel-polyfill"
import React from 'react'
import ReactDOM from 'react-dom'
import {
createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from "redux-saga";
import Counter from './Counter'
import reducer from './reducers'
import rootSaga from "./sagas";
// create the saga middleware
const sagaMiddleware=createSagaMiddleware();
// mount it on the Store
const store = createStore(reducer,applyMiddleware(sagaMiddleware))
const action = type => store.dispatch({
type})
function render() {
ReactDOM.render(
<Counter
value={
store.getState()}
onIncrement={
() => action('INCREMENT')}
onDecrement={
() => action('DECREMENT')}
onIncrementAsync={
() => action('INCREMENT_ASYNC')} />,
document.getElementById('root')
)
}
// then run the saga
sagaMiddleware.run(rootSaga);
// render the application
render();
//subscribe the render
store.subscribe(render)
📌3.修改 sagas.js 模块:
import {
delay } from "redux-saga";
import {
put,takeEvery,all } from "redux-saga/effects";
function* helloSaga(){
console.log('hello Sagas');
}
function* incrementAsync(){
yield delay(1000)
yield put({
type:'INCREMENT'})
}
function* watchIncrementAsync(){
yield takeEvery('INCREMENT_ASYNC',incrementAsync)
}
export default function* rootSaga(){
yield all([helloSaga(),watchIncrementAsync()])
}
watchIncrementAsync 用于监听所有的 INCREMENT_ASYNC action,并在 action 被匹配时执行 incrementAsync 任务。
为什么不直接传递incrementAsync
?直接传一开始调用就被执行,根本不会实现监听效果。
创建另一个文件 sagas.spec.js:
import test from 'tape';
import {
put, call } from 'redux-saga/effects'
import {
delay } from 'redux-saga'
//import { incrementAsync } from './sagas'
function * incrementAsync(){
yield call(delay,1000)
yield put({
type:'INCREMENT'})
}
test('incrementAsync Saga test', (assert) => {
const gen = incrementAsync()
assert.deepEqual(
gen.next().value,
call(delay, 1000),
'incrementAsync Saga must call delay(1000)'
)
assert.deepEqual(
gen.next().value,
put({
type: 'INCREMENT'}),
'incrementAsync Saga must dispatch an INCREMENT action'
)
assert.deepEqual(
gen.next(),
{
done: true, value: undefined },
'incrementAsync Saga must be done'
)
assert.end()
});
测试: npm test
📌解惑
incrementAsync 是一个 Generator 函数。执行的时候返回一个 iterator object,这个 iterator 的 next 方法返回一个如下格式的对象:
gen.next() // => { done: boolean, value: any }
value
是 yield 后面那个表达式的结果。done
字段指示 generator 是否结束了。
在 incrementAsync 的例子中,generator 连续 yield 了两个值:
所以,如果我们连续 3 次调用 generator 的 next 方法,我们会得到以下结果:
gen.next() // => { done: false, value: <result of calling delay(1000)> }
gen.next() // => { done: false, value: <result of calling put({type: 'INCREMENT'})> }
gen.next() // => { done: true, value: undefined }
可见,delay的值还是个Promise对象,而且处于pending状态,很难进行测试比对。那有没有办法将 delay 返回的值变为一个 普通 的值呢。
redux-saga 提供了一种方式,与在 incrementAsync 中直接(directly)调用 delay(1000) 不同,我们叫它 indirectly:
export function* incrementAsync() {
// use the call Effect
yield call(delay, 1000)
yield put({
type: 'INCREMENT' })
}
我们现在做的是 yield call(delay, 1000) 而不是 yield delay(1000),所以有何不同?
在 yield delay(1000) 的情况下,yield 后的表达式 delay(1000) 在被传递给 next 的调用者之前就被执行了(当运行我们的代码时,调用者可能是 middleware。 也有可能是运行 Generator 函数并对返回的 Generator 进行迭代的测试代码)。所以调用者得到的是一个 Promise<Pending>
,像在以上的测试代码里一样。
而在 yield call(delay, 1000) 的情况下,yield 后的表达式 call(delay, 1000) 被传递给 next 的调用者。call 就像 put, 返回一个 Effect,告诉 middleware 使用给定的参数调用给定的函数。实际上,无论是 put 还是 call 都不执行任何 dispatch 或异步调用,它们只是简单地返回 plain Javascript 对象。
put({
type: 'INCREMENT'}) // => { PUT: {type: 'INCREMENT'} }
call(delay, 1000) // => { CALL: {fn: delay, args: [1000]}}
这里发生的事情是:middleware 检查每个被 yield 的 Effect 的类型,然后决定如何实现哪个 Effect。如果 Effect 类型是 PUT 那 middleware 会 dispatch 一个 action 到 Store。 如果 Effect 类型是 CALL 那么它会调用给定的函数。
redux-saga 使用 PUT 来描述dispatch 一个 action 到 Store 而不是直接dispatch action 的原因也是为了方便测试。
function* fetchProducts(dispatch)
const products = yield call(Api.fetch, '/products')
dispatch({
type: 'PRODUCTS_RECEIVED', products })
}
直接dispatch的话在next()的时候可能还是Promise<Pending>
,所以
function* fetchProducts() {
const products = yield call(Api.fetch, '/products')
// 创建并 yield 一个 dispatch Effect
yield put({
type: 'PRODUCTS_RECEIVED', products })
}
try/catch
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 })
}
}
Promise catch
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 })
}
Saga 辅助函数 构建在 Effect 创建器之上的辅助函数。(即高级 API)
Effect 创建器 以下每个函数都会返回一个普通 Javascript 对象(plain JavaScript object),并且不会执行任何其它操作。 执行是由 middleware 在迭代过程中进行的。 middleware 会检查每个 Effect 的描述信息,并进行相应的操作
创建一个 Redux middleware,并将 Sagas 连接到 Redux Store。options可选项,感觉用的不多,主要涉及sagaMonitor, emitter , onError ,有兴趣的可自行了解。
import createSagaMiddleware from "redux-saga";
const sagaMiddleware=createSagaMiddleware();
动态地运行 saga。只能 用于在 applyMiddleware 阶段 之后 执行 Saga。
const store = createStore(reducer,applyMiddleware(sagaMiddleware))
...
sagaMiddleware.run(rootSaga);
saga 必须是一个返回 Generator 对象 的函数。middleware 会迭代这个 Generator 并执行所有 yield 后的 Effect。
在第一次迭代里,middleware 会调用 next()
方法来获取下一个 Effect。与此同时,Generator 将被暂停,直到 effect 执行结束。在接收到执行的结果时,middleware 在 Generator 里接着调用 next(result),并将得到的结果作为参数传入。 这个过程会一直重复,直到 Generator 正常终止或抛出错误。
在发起(dispatch)到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。
import {
takeEvery } from `redux-saga/effects`
function* fetchUser(action) {
...
}
function* watchFetchUser() {
yield takeEvery('USER_REQUESTED', fetchUser)
}
📌注意
takeEvery 是一个使用 take
和 fork
构建的高级 API。下面演示了这个辅助函数是如何由低级 Effect 实现的:
const takeEvery = (patternOrChannel, saga, ...args) => fork(function*() {
while (true) {
const action = yield take(patternOrChannel)
yield fork(saga, ...args.concat(action))
}
})
创建一个 Effect 描述信息,用来命令 middleware 在 Store 上等待指定的 action。 在发起与 pattern 匹配的 action 之前,该saga处于暂停状态,直到任意的一个 action 被发起。
import {
select, take } from 'redux-saga/effects'
function* watchAndLog() {
while (true) {
const action = yield take('*')
const state = yield select()
console.log('action', action)
console.log('state after', state)
}
}
middleware 提供了一个特殊的 action —— END
。如果你发起 END action,则无论哪种 pattern,只要是被 take Effect 阻塞的 Sage 都会被终止。假如被终止的 Saga 下仍有分叉(forked)任务还在运行,那么它在终止任务前,会先等待其所有子任务均被终止。
take.maybe(pattern)
与 take(pattern) 相同,但在 END
action 时不自动地终止 Saga。与所有在 take Effect 上阻塞的 Saga 都将获得 END 对象的规则相反。
在 takeEvery 的情况中,被调用的任务无法控制何时被调用, 它们将在每次 action 被匹配时一遍又一遍地被调用。并且它们也无法控制何时停止监听。
而在 take 的情况中,控制恰恰相反。与 action 被 推向(pushed) 任务处理函数不同,Saga 是自己主动 拉取(pulling) action 的。 看起来就像是 Saga 在执行一个普通的函数调用 action = getNextAction()
,这个函数将在 action 被发起时 resolve。
这样的反向控制让我们可以使用传统的 push 方法实现不同的控制流程。
1.一个简单的例子,假设在我们的 Todo 应用中,我们希望监听用户的操作,并在用户初次创建完三条 Todo 信息时显示祝贺信息。
import {
take, put } from 'redux-saga/effects'
function* watchFirstThreeTodosCreation() {
for (let i = 0; i < 3; i++) {
const action = yield take('TODO_CREATED')
}
yield put({
type: 'SHOW_CONGRATULATION'})
}
2.使用拉取(pull)模式,我们可以在同一个地方写控制流,而不是重复处理相同的 action。
function* loginFlow() {
while (true) {
yield take('LOGIN')
// ... perform the login logic
yield take('LOGOUT')
// ... perform the logout logic
}
}
创建一个 Effect 描述信息,用来命令 middleware 向 Store 发起一个 action。 这个 effect 是非阻塞型的,并且所有向下游抛出的错误(例如在 reducer 中),都不会冒泡回到 saga 当中。
import {
call,put } from "redux-saga/effects";
function * incrementAsync(){
yield call(delay,1000)
yield put({
type:'INCREMENT'})
}
创建一个 Effect 描述信息,用来命令 middleware 以参数 args 调用函数 fn ,阻塞的。
创建一个 Effect 描述信息,用来命令 middleware 以 非阻塞调用 的形式执行 fn。
返回一个 Task 对象。
📌注意
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'))
}
}
创建一个 Effect 描述信息,用来命令 middleware 取消之前的一个分叉任务。
创建一个 Effect,用来命令 middleware 在当前 Store 的 state 上调用指定的选择器(即返回 selector(getState(), …args) 的结果)。
创建一个 Effect 描述信息,用来命令 middleware 在多个 Effect 间运行 竞赛(Race),只会返回最快完成的哪个Effect的结果。当 resolve race 的时候,middleware 会自动地取消所有输掉的 Effect。
import {
take, call, race } from `redux-saga/effects`
import fetchUsers from './path/to/fetchUsers'
function* fetchUsersSaga {
const {
response, cancel } = yield race({
response: call(fetchUsers),
cancel: take(CANCEL_FETCH)
})
}
创建一个 Effect 描述信息,用来命令 middleware 并行地运行多个 Effect,并等待它们全部完成。
当并发运行 Effect 时,middleware 将暂停 Generator,直到以下任一情况发生:
import {
fetchCustomers, fetchProducts } from './path/to/api'
import {
all, call } from `redux-saga/effects`
function* mySaga() {
const [customers, products] = yield all([
call(fetchCustomers),
call(fetchProducts)
])
}
概括来说,从 Saga 内触发异步操作(Side Effect)总是由 yield
一些声明式的 Effect 来完成的,Effect是一个 普通js对象,包含一些将被 saga middleware 执行的指令。Effect 是使用 redux-saga 提供的工厂函数创建的。完整列表的 声明式的 Effect 可在这里找到: API reference
put({
type: 'INCREMENT'}) // => { PUT: {type: 'INCREMENT'} }
call(delay, 1000) // => { CALL: {fn: delay, args: [1000]}}
如:call(delay, 1000)
指示 middleware 调用 delay(1000)
并将结果返回给 yield effect 的那个 Generator。
当然你也可以yield
一个 Promise来完成异步操作,但是这会让测试变得困难。
一个 task 就像是一个在后台运行的进程。在基于 redux-saga 的应用程序中,可以同时运行多个 task。通过 fork 函数来创建 task:
function* saga() {
...
const task = yield fork(otherSaga, ...args)
...
}
阻塞调用的意思是,Saga 在 yield Effect 之后会等待其执行结果返回,结果返回后才会恢复执行 Generator 中的下一个指令。
非阻塞调用的意思是,Saga 会在 yield Effect 之后立即恢复执行。
function* saga() {
yield take(ACTION) // 阻塞: 将等待 action
yield call(ApiFn, ...args) // 阻塞: 将等待 ApiFn (如果 ApiFn 返回一个 Promise 的话)
yield call(otherSaga, ...args) // 阻塞: 将等待 otherSaga 结束
yield put(...) // 阻塞: 将同步发起 action (使用 Promise.then)
const task = yield fork(otherSaga, ...args) // 非阻塞: 将不会等待 otherSaga
yield cancel(task) // 非阻塞: 将立即恢复执行
// or
yield join(task) // 阻塞: 将等待 task 结束
}
指的是一种使用两个单独的 Saga 来组织控制流的方式。
function* watcher() {
while(true) {
const action = yield take(ACTION)
yield fork(worker, action.payload)
}
}
function* worker(payload) {
// ... do some stuff
}
================================================================================================== 参考文档: redux-saga官网 Redux-saga
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/191814.html原文链接:https://javaforall.cn