大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺
如果redux需要用到 side effect 异步操作,redux-thunk 和 redux-saga 绝对是目前两个最受欢迎的中间件插件。
redux-saga |
redux-saga是一个用于管理redux应用异步操作的中间件,redux-saga通过创建sagas将所有异步操作逻辑收集在一个地方集中处理,可以用来代替redux-thunk中间件。
这意味着应用的逻辑会存在两个地方:
- reducer负责处理action的state更新
- sagas负责协调那些复杂或者异步的操作
React+Redux Cycle(来源:https://www.youtube.com/watch?v=1QI-UE3-0PU)
Sagas 特点 |
- sagas是通过generator函数来创建的
- sagas可以被看作是在后台运行的进程。sagas监听发起的action,然后决定基于这个action来做什么 (比如:是发起一个异步请求,还是发起其他的action到store,还是调用其他的sagas 等 )
- 在redux-saga的世界里,所有的任务都通过用 yield Effects 来完成 ( effect可以看作是redux-saga的任务单元 )
- redux-saga启动的任务可以在任何时候通过手动来取消,也可以把任务和其他的Effects放到 race 方法里以自动取消
redux-saga 使用了 ES6 的 Generator
功能,让异步的流程更易于读取,写入和测试。不同于 redux thunk,你不会再遇到回调地狱了,你可以很容易地测试异步流程并保持你的 action 是干净的。(Generator可以通过next查看每一步的调用结果)
Hello redux-saga |
主要根据官方案例构建
初始化项目 |
?1.克隆教程仓库
git clone https://github.com/redux-saga/redux-saga-beginner-tutorial.git
?2.安装依赖
cd redux-saga-beginner-tutorial
npm install
此时项目结构是这样的:
?3.启动应用
npm start
页面效果:
Hello,Sagas! |
?1.创建一个 sagas.js 的文件,然后添加以下代码片段:
export function* helloSaga() {
console.log('Hello Sagas!');
}
为了运行我们的 Saga,我们需要:
- 创建一个 Saga middleware
- 运行的 Sagas(目前我们只有一个 helloSaga)
- 将这个 Saga middleware 连接至 Redux store.
?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 了两个值:
- yield delay(1000)
- yield put({type: ‘INCREMENT’})
所以,如果我们连续 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>
,像在以上的测试代码里一样。
声明式 Effects |
而在 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 })
}
常用API |
Saga 辅助函数 构建在 Effect 创建器之上的辅助函数。(即高级 API)
Effect 创建器
以下每个函数都会返回一个普通 Javascript 对象(plain JavaScript object),并且不会执行任何其它操作。
执行是由 middleware 在迭代过程中进行的。 middleware 会检查每个 Effect 的描述信息,并进行相应的操作
createSagaMiddleware(options) |
创建一个 Redux middleware,并将 Sagas 连接到 Redux Store。options可选项,感觉用的不多,主要涉及sagaMonitor, emitter , onError ,有兴趣的可自行了解。
import createSagaMiddleware from "redux-saga";
const sagaMiddleware=createSagaMiddleware();
middleware.run(saga, …args) |
动态地运行 saga。只能 用于在 applyMiddleware 阶段 之后 执行 Saga。
- saga: Function: 一个 Generator 函数
- args: Array: 提供给 saga 的参数
const store = createStore(reducer,applyMiddleware(sagaMiddleware))
...
sagaMiddleware.run(rootSaga);
saga 必须是一个返回 Generator 对象 的函数。middleware 会迭代这个 Generator 并执行所有 yield 后的 Effect。
middleware 迭代 |
在第一次迭代里,middleware 会调用 next()
方法来获取下一个 Effect。与此同时,Generator 将被暂停,直到 effect 执行结束。在接收到执行的结果时,middleware 在 Generator 里接着调用 next(result),并将得到的结果作为参数传入。 这个过程会一直重复,直到 Generator 正常终止或抛出错误。
takeEvery(pattern, saga, …args) |
在发起(dispatch)到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。
- 允许处理并发的 action
- 不会阻塞
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))
}
})
take(pattern) |
创建一个 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
}
}
put(action) |
创建一个 Effect 描述信息,用来命令 middleware 向 Store 发起一个 action。 这个 effect 是非阻塞型的,并且所有向下游抛出的错误(例如在 reducer 中),都不会冒泡回到 saga 当中。
import {
call,put } from "redux-saga/effects";
function * incrementAsync(){
yield call(delay,1000)
yield put({
type:'INCREMENT'})
}
call(fn, …args) |
创建一个 Effect 描述信息,用来命令 middleware 以参数 args 调用函数 fn ,阻塞的。
- fn: Function – 一个 Generator 函数, 也可以是一个返回 Promise 或任意其它值的普通函数。
- args: Array – 传递给 fn 的参数数组。
fork(fn, …args) |
创建一个 Effect 描述信息,用来命令 middleware 以 非阻塞调用 的形式执行 fn。
- fn: Function – 一个 Generator 函数,或返回 Promise 的普通函数
- args: Array – 传递给 fn 的参数数组。
返回一个 Task 对象。
?注意
-
fork 类似于 call,fork 的调用是非阻塞的,
-
yield fork(fn …args) 的结果是一个 Task 对象 —— 一个具备着某些实用方法及属性的对象。
-
所有分叉任务(forked tasks)都会被附加(attach)到它们的父级任务身上。当父级任务终止其自身命令的执行,它会在返回之前等待所有分叉任务终止。
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'))
}
}
cancel(task) |
创建一个 Effect 描述信息,用来命令 middleware 取消之前的一个分叉任务。
select(selector, …args) |
创建一个 Effect,用来命令 middleware 在当前 Store 的 state 上调用指定的选择器(即返回 selector(getState(), …args) 的结果)。
-
selector: Function – 一个 (state, …args) => args 的函数。它接受当前 state 和一些可选参数,并返回当前 Store state 上的一部分数据。
-
args: Array – 传递给选择器的可选参数,将追加在 getState 后。
-
如果调用 select 的参数为空(即 yield select()),那么 effect 会取得完整的 state(与调用 getState() 的结果相同)。
race(effects) |
创建一个 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)
})
}
all([…effects]) |
创建一个 Effect 描述信息,用来命令 middleware 并行地运行多个 Effect,并等待它们全部完成。
当并发运行 Effect 时,middleware 将暂停 Generator,直到以下任一情况发生:
-
所有 Effect 都成功完成:返回一个包含所有 Effect 结果的数组,并恢复 Generator。
-
在所有 Effect 完成之前,有一个 Effect 被 reject:在 Generator 中抛出 reject 错误。
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)
])
}
名词解释 |
Effect |
概括来说,从 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 |
一个 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 结束
}
Watcher/Worker |
指的是一种使用两个单独的 Saga 来组织控制流的方式。
-
Watcher: 监听发起的 action 并在每次接收到 action 时 fork 一个 worker。
-
Worker: 处理 action 并结束它。
function* watcher() {
while(true) {
const action = yield take(ACTION)
yield fork(worker, action.payload)
}
}
function* worker(payload) {
// ... do some stuff
}
==================================================================================================
参考文档:
redux-saga官网
Redux-saga
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/191814.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...