redux-saga学习

redux-saga学习如果redux需要用到sideeffect异步操作,redux-thunk和redux-saga绝对是目前两个最受欢迎的中间件插件。redux-saga是一个用于管理redux应用异步操作的中间件,redux-saga通过创建sagas将所有异步操作逻辑收集在一个地方集中处理,可以用来代替redux-thunk中间件。

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新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 是一个使用 takefork 构建的高级 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账号...

(0)
blank

相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号