博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
redux-saga 初级学习教程
阅读量:4085 次
发布时间:2019-05-25

本文共 7286 字,大约阅读时间需要 24 分钟。

redux-saga 初级学习教程

先从 redux 的三原则谈起:

  • 唯一的状态源:Store
  • 唯一触发状态修改的方法: Action
  • 唯一转换状态的纯函数: Reducer

redux.png

三者中可以添加异步操作的地方只有 store.dispatch(action) 这个环节了,这也是中间件编写的位置。那么是否可以借鉴 redux 的唯一原则将异步操作逻辑统一处理?redux-saga 的设计理念正是如此。

1. 见证 saga

1.1. 聚合异步操作

不同于 redux-thunk 和类似功能的 redux-promise 等中间件,redux-saga 可以被看作是后台运行的进程,监听发起的 action ,决定该 action 是进行异步调用,还是触发其他的 action 发送到 Store 。这样的机制使得在设计应用时可以将异步操作逻辑进行统一管理。

saga.png

1.2. 官方初级教程

$ 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 按钮减少数字。

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'  // 导入 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)// ...

完工!看看效果。

当然还是需要一些解释的。

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_USERSfunction* fetchUsers(action) { ... }// CREATE_USERfunction* createUser(action) { ... }// use them in parallelexport 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 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

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 })}

作者:wlszouc
链接:https://www.jianshu.com/p/f3c7594c4fb4
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

你可能感兴趣的文章
Socket经验记录
查看>>
对RTMP视频流进行BitmapData.draw()出错的解决办法
查看>>
FMS 客户端带宽计算、带宽限制
查看>>
在线视频聊天(客服)系统开发那点事儿
查看>>
SecurityError Error 2148 SWF 不能访问本地资源
查看>>
烈焰SWF解密
查看>>
Qt 静态编译后的exe太大, 可以这样压缩.
查看>>
3D游戏常用技巧Normal Mapping (法线贴图)原理解析——基础篇
查看>>
乘法逆元
查看>>
STL源码分析----神奇的 list 的 sort 算法实现
查看>>
Linux下用math.h头文件
查看>>
Linux中用st_mode判断文件类型
查看>>
Ubuntu修改host遇到unable to resolve host
查看>>
路由选择算法
查看>>
Objective-C 基础入门(一)
查看>>
Objective-C 基础入门(三) 读写文件与回调
查看>>
C++ STL标准库与泛型编程(一)概述
查看>>
C++ STL标准库与泛型编程(四)Deque、Queue、Stack 深度探索
查看>>
C++ STL标准库 算法
查看>>
JVM内存模型_Minor GC笔记
查看>>