Redux-saga

Redux-saga

概述

redux 一文中我们有说过处理异步我们应该放在 reducer 之前,所以我们需要中间件来处理我们的异步操作。redux-saga 就是一个用于管理 redux 应用的异步操作中间件,redux-saga 通过创建 sagas 将所有异步操作的逻辑都收集在一个位置集中处理。

简单的来说就是两步:

  • sagas 负责协调那些复杂或者异步的操作
  • reducer 负责处理 actionstage 更新

数据流

我们先看一张图来大致了解下整个过程:

6493119-f4c44eb340874014.png

有了之前 redux 的基础,这张图其实还是比较容易理解的,下面我们来简单说下:

  1. React 组件由用户触发事件,通过 action creator 发起 action
  2. sagas 监听发起的 action,然后决定基于这个 action 做什么
  3. 紧接着根据上步所做处理将 effects(sage的任务单元,简单的 JavaScript 对象)传给 reducer
  4. 最后由 reducer 返回新的 state

安装

yarn add redux-saga 
// npm install redux-saga -S

使用实例

配置

import { createStore, combineReducers, applyMiddleware } from 'redux';
import global from './reducers/global';
import login from './reducers/login';
import createSagaMiddleware from 'redux-saga';       // 引入redux-saga中的createSagaMiddleware函数
import rootSaga from '../sagas';                    // 引入saga.js

const sagaMiddleware = createSagaMiddleware()        // 执行

const reducerAll = {
  global,
  login
}


export const store = createStore(
    combineReducers({...reducerAll}),               // 合并reducer
    applyMiddleware(sagaMiddleware)                 // 中间件,加载sagaMiddleware
)

sagaMiddleware.run(rootSaga)

ui组件触发action创建函数

handleSubmit = (e) => {
    this.props.form.validateFields((err, values) => {
      if (!err) {
        console.log('Received values of form: ', values);
        this.props.onSubmit(values);
      }
    });
  }

<Form onSubmit={this.handleSubmit} className="login-form">
    ...
</Form>

将action传入saga

const mapDispatchToProps = dispatch => {
  return {
    onSubmit: (payload) => {
      dispatch({ type: LOGIN_REQUEST, ...payload })
    }
  }
}

saga捕获action创建函数返回的action(effects)

function* fetchLogin(userName, password) {
  try {
    console.log("saga:");
    const token = yield call(axios.get({ url: '' }))
    yield put({ type: 'LOGIN_SUCCESS', token })
  } catch(error) {
    yield put({ type: 'LOGIN_ERROR', error })
  }
}

export default function* watchIsLogin(dispatch) {
  while(true) {
    const { userName, password } = yield take('LOGIN_REQUEST');
    yield fork(fetchLogin, userName, password)
  }
}

reducer接受并返回新的state

const initState = {
  userName: 'Cola'
}

export default function(state = initState, action){
  switch (action.type) {
    case LOGIN_IN:
      return {...state, ...action.payload};
    default:
      return state;
  }
}

看了上面的步骤是不是觉得整体逻辑还是非常清晰明了的,下面我们把上面代码中未讲解到的代码名词解释下。

名词解释

Effect

一个 effect 就是一个纯文本 javascript 对象,包含一些将被 saga middleware 执行的指令。那么如何创建 effect 呢?使用下面我们马上要讲到API中 redux-saga 提供的工厂函数来创建 effect

Task

一个 task 就像是一个在后台运行的进程。在基于 redux-saga 的应用程序中,可以同时运行多个 task。通过 fork 函数来创建 task

阻塞调用/非阻塞调用

阻塞调用的意思是,Sagayield 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)                   // 非阻塞
  yield put(channel, ACTION)                   // 当 put 没有被缓存而是被 taker 立即消费掉时,阻塞

  const task = yield fork(otherSaga, ...args)  // 非阻塞: 将不会等待 otherSaga
  yield cancel(task)                           // 非阻塞: 将立即恢复执行
  // or
  yield join(task)                             // 阻塞: 将等待 task 结束
}

Watcher/Worker

指的是一种使用两个单独的 Saga 来组织控制流的方式。

  • Watcher: 监听发起的 action 并在每次接收到 actionfork 一个 worker
  • Worker: 处理 action 并结束它。
function* watcher() {
  while(true) {
    const action = yield take(ACTION)
    yield fork(worker, action.payload)
  }
}

function* worker(payload) {
  // ... do some stuff
}

Middleware API

createSagaMiddleware(options)

创建一个 Redux middleware,并将 Sagas 连接到 Redux Store

options : Object - 传递给 middleware 的选项列表。

middleware.run(saga, ...args)

动态执行 saga。用于 applyMiddleware 阶段之后执行 Sagas。这个方法返回一个
Task 描述对象。

Saga辅助函数

takeEvery

在发起(dispatch)到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。简单来说就是监听所有的匹配到的 action

import { takeEvery } from `redux-saga/effects`

function* fetchUser(action) {
  ...
}

function* watchFetchUser() {
  yield takeEvery('USER_REQUESTED', fetchUser)
}

takeLatest

在发起到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。并自动取消之前所有已经启动但仍在执行中的 saga 任务。

import { takeLatest } from `redux-saga/effects`

function* fetchUser(action) {
  ...
}

function* watchLastFetchUser() {
  yield takeLatest('USER_REQUESTED', fetchUser)
}

这样便可以保证:即使用户以极快的速度连续多次触发 USER_REQUESTED action,我们都只会以最后的一个结束。

takeLeading

在发起到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。 它将在派生一次任务之后阻塞,直到派生的 saga 完成,然后又再次开始监听指定的 pattern

import { takeLeading } from `redux-saga/effects`

function* fetchUser(action) {
  ...
}

function* watchLastFetchUser() {
  yield takeLeading('USER_REQUESTED', fetchUser)
}

由于 takeLeading 在其开始之后便无视所有新传入的任务,我们便可以保证:如果用户以极快的速度连续多次触发 USER_REQUESTED action,我们都只会保持以第一个 action 运行。

throttle

它在派生一次任务之后,仍然将新传入的 action 接收到底层的 buffer 中,至多保留(最近的)一个。但与此同时,它在 ms 毫秒内将暂停派生新的任务 —— 这也就是它被命名为节流阀(throttle)的原因。其用途,是在处理任务时,无视给定的时长内新传入的 action

import { call, put, throttle } from `redux-saga/effects`

function* fetchAutocomplete(action) {
  const autocompleteProposals = yield call(Api.fetchAutocomplete, action.text)
  yield put({type: 'FETCHED_AUTOCOMPLETE_PROPOSALS', proposals: autocompleteProposals})
}

function* throttleAutocomplete() {
  yield throttle(1000, 'FETCH_AUTOCOMPLETE', fetchAutocomplete)
}

上面的代码我们通过 throttle 无视了一段时间内连续的 FETCH_AUTOCOMPLETE,我们便可以确保用户不会因此向我们的服务器发起大量请求。

Effect 创建器

注意:

  • 以下每个函数都会返回一个普通 Javascript 对象(plain JavaScript > > object),并且不会执行任何其它操作。
  • 执行是由 middleware 在上述迭代过程中进行的。
  • middleware 会检查每个 Effect 的描述信息,并进行相应的操作

take

创建一个 Effect 描述信息,用来命令 middlewareStore 上等待指定的 action。 在发起匹配的 action 之前,Generator 将暂停。

put

创建一个 Effect 描述信息,用来命令 middlewareStore 发起一个 action。 这个 effect 是非阻塞型的,并且所有向下游抛出的错误(例如在 reducer 中),都不会冒泡回到 saga 当中。

call(fn, ...args)

创建一个 Effect 描述信息,用来命令 middleware 以参数 args 调用函数 fn

fork(fn, ...args)

创建一个 Effect 描述信息,用来命令 middleware非阻塞调用 的形式执行 fn

join(task)

创建一个 Effect 描述信息,用来命令 middleware 等待之前的一个分叉任务的结果。

cancel(task)

创建一个 Effect 描述信息,用来命令 middleware 取消之前的一个分叉任务。

select(selector, ...args)

创建一个 Effect,用来命令 middleware 在当前 Storestate 上调用指定的选择器(即返回 selector(getState(), ...args) 的结果)

例如,假设我们在应用程序中有这样结构的一份 state

state = {
  cart: {...}
}

我们创建一个 选择器(selector),即一个知道如果从 State 中提取 cart 数据的函数:

// selectors.js
export const getCart = state => state.cart

然后,我们可以使用 select EffectSaga 的内部使用该选择器:

import { take, fork, select } from 'redux-saga/effects'
import { getCart } from './selectors'

function* checkout() {
  // 使用被导出的选择器查询 state
  const cart = yield select(getCart)

  // ... 调用某些 API,然后发起一个 success/error action
}

export default function* rootSaga() {
  while (true) {
    yield take('CHECKOUT_REQUEST')
    yield fork(checkout)
  }
}

actionChannel(pattern, [buffer])

创建一个 Effect,用来命令 middleware 通过一个事件 channel 对匹配 patternaction 进行排序。 作为可选项,你也可以提供一个 buffer 来控制如何缓存排序的 actions

import { actionChannel, call } from 'redux-saga/effects'
import api from '...'

function* takeOneAtMost() {
  const chan = yield actionChannel('USER_REQUEST')
  while (true) {
    const {payload} = yield take(chan)
    yield call(api.getUser, payload)
  }
}

flush(channel)

创建一个 Effect,用来命令 middlewarechannel 中冲除所有被缓存的数据。被冲除的数据会返回至 saga,这样便可以在需要的时候再次被利用。

function* saga() {
  const chan = yield actionChannel('ACTION')

  try {
    while (true) {
      const action = yield take(chan)
      // ...
    }
  } finally {
    const actions = yield flush(chan)
    // ...
  }

}

cancelled()

创建一个 Effect,用来命令 middleware 返回该 generator 是否已经被取消。通常你会在 finally 区块中使用这个 Effect 来运行取消时专用的代码。

function* saga() {
  try {
    // ...
  } finally {
    if (yield cancelled()) {
      // 只应在取消时执行的逻辑
    }
    // 应在所有情况下都执行的逻辑(例如关闭一个 channel)
  }
}

setContext(props)

创建一个 effect,用来命令 middleware 更新其自身的上下文。这个 effect 扩展了 saga 的上下文,而不是代替。

getContext(prop)

创建一个 effect,用来命令 middleware 返回 saga 的上下文中的一个特定属性。

Effect 组合器

race(effects)

创建一个 Effect 描述信息,用来命令 middleware 在多个 Effect 间运行 竞赛(Race)(与 Promise.race([...]) 的行为类似)。

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,并等待它们全部完成。这是与标准的 Promise#all 相当对应的 API

import { fetchCustomers, fetchProducts } from './path/to/api'
import { all, call } from `redux-saga/effects`

function* mySaga() {
  const { customers, products } = yield all({
    customers: call(fetchCustomers),
    products: call(fetchProducts)
  })
}

接口

Task

方法 返回值
task.isRunning() 若任务还未返回或抛出了一个错误则为 true
task.isCancelled() 若任务已被取消则为 true
task.result() 任务的返回值。若任务仍在运行中则为 undefined
task.error() 任务抛出的错误。若任务仍在执行中则为 undefined
task.done 一个 Promise
task.cancel() 取消任务(如果任务仍在执行中)

Channel

方法 返回值
Channel.take(callback) 用于注册一个 taker
Channel.put(message) 用于在 buffer 上放入消息
Channel.flush(callback) 用于从 channel 中提取所有被缓存的消息
Channel.close() 关闭 channel,意味着不再允许做放入操作

Buffer

方法 返回值
Buffer.isEmpty() 如果缓存中没有消息则返回
Buffer.put(message) 用于往缓存中放入新的消息
Buffer.take() 用于检索任何被缓存的消息

SagaMonitor

用于由 middleware 发起监视(monitor)事件。实际上,middleware 发起 5 个事件:

  • 当一个 effect 被触发时(通过 yield someEffect),middleware 调用 sagaMonitor.effectTriggered(options) : options 包括:

    • effectId : Number - 分配给 yielded effect 的唯一 ID

    • parentEffectId : Number - 父级 EffectID。在 raceparallel effect 的情况下,所有在内部 yieldeffect 都将有一个直接 race/parallel 的父级 effect。在最顶级的 effect 的情况下,父级是包裹它的 Saga

    • label : String - 在 race effect 的情况下,所有子 effect 都将被指定为传递给 race 的对象中对应键的标签。

    • effect : Object - yielded effect 其自身

  • 如果该 effect 成功地被 resolve,则 middleware 调用 sagaMonitor.effectResolved(effectId, result)

    • effectId : Number - yielded effectID

    • result : any - 该 effect 成功 resolve 的结果。在 forkspawn 的情况下,结果将是一个 Task 对象。

  • 如果该 effect 因一个错误被 reject,则 middleware 调用 sagaMonitor.effectRejected

    • effectId : Number - yielded effectID

    • error : any - 该 effect reject 的错误

  • 如果该 effect 被取消,则 middleware 调用 sagaMonitor.effectCancelled

    • effectId : Number - yielded effectID
  • 最后,当 Redux action 被发起时,middleware 调用 sagaMonitor.actionDispatched

    • action : Object - 被发起的 Redux action。如果该 action 是由一个 Saga 发起的,那么该 action 将拥有一个属性 SAGA_ACTION 并被设为 true(你可以从 redux-saga/utils 中导入 SAGA_ACTION)。

外部API

不做赘述,可根据自己需要使用

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

推荐阅读更多精彩内容

  • Redux-saga 概述 redux-saga是一个用于管理redux应用异步操作的中间件,redux-saga...
    woow_wu7阅读 51,656评论 11 41
  • redux-saga框架使用详解及Demo教程 前面我们讲解过redux框架和dva框架的基本使用,因为dva框架...
    光强_上海阅读 22,032评论 8 46
  • 前端时间一直在补大学的知识,很惭愧==,今天带来一个项目中用的知识,目前的技术栈是ant design + dva...
    tobAlier阅读 2,404评论 0 3
  • 1. redux-thunk处理副作用的缺点 1.1 redux的副作用处理 redux中的数据流大致是: UI—...
    Grace_ji阅读 3,535评论 0 14
  • 开封位于中原腹地的豫东平原,具有悠久历史,曾为七朝古都。北宋时名东京、汴京,达到鼎盛辉煌,极其繁荣,是当时世界特大...
    胡老太阅读 1,832评论 31 50