redux-saga 初级学习教程

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

打开 http://localhost:9966/ ,如果一切正常浏览器中将显示:

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

const 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 }) =>
  <div>
  ...
    {' '}
    <button onClick={onIncrementAsync}>
      Increment after 1 second
    </button>
    <hr />
    <div>
      Clicked: {value} times
    </div>
  </div>

将方法添加到父组件 main.js

function render() {
  ReactDOM.render(
    <Counter
      value={store.getState()}
      onIncrement={() => 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(),将它们统一输出给运行函数:

// 同时执行一个入口的多个 Sagas
export 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_USERS
function* fetchUsers(action) { ... }

// CREATE_USER
function* createUser(action) { ... }

// use them in parallel
export 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 instruction
assert.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 })
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容