Redux 异步操作redux-thunk和redux-saga

Redux 异步操作redux-thunk和redux-saga

Redux中的action仅支持原始对象(plain object),处理有副作用的action,需要使用中间件。中间件可以在发出action,到reducer函数接受action之间,执行具有副作用的操作,例如网络请求,读取浏览器缓存等。

redux-thunk

redux-thunkredux的作者提供的一个处理副作用的方案,我们都知道,dispatch必须接收一个原始对象,在里面无法实现副作用逻辑,但是我们可以使用action creator函数,在内部处理副作用,然后返回一个action原始对象。

安装

yarn add redux-thunk

应用redux-thunk中间件

import thunk from 'redux-thunk'
// ...
let store = createStore(reducers, applyMiddleware(thunk))

使用

此时我们不能直接派发action,而是先要创建一个函数,例如下面的changeName,函数签名dispatchgetState,都是store上的两个方法,然后可以在里面进行异步请求操作

const ConnectedUser = connect(mapStateToProps, {
    ageIncrement(payload) {
        return {type: 'increment', payload}
    },
    ageDecrement(payload) {
        return {type: 'decrement', payload}
    },
    changeName(payload) {
        return (dispatch, getState) => {
           fetch('https://jsonplaceholder.typicode.com/posts')
            .then(res => res.json())
            .then(data => dispatch({type: 'changeName', payload} ))
        }
    }
})(User)

redux-saga

redux-sagaredux处理副作用的另一种方式,相较于redux-thunk,能更好的的组织代码,功能也更加丰富

安装

yarn add redux-saga

使用

应用中间件

import createSagaMidware from 'redux-saga'
const saga = createSagaMidware()

const store = createStore(reducers, applyMiddleware(saga))

redux-saga采用的方案更接近于redux的全局思想,使用方式和thunk有很大不同,
saga需要一个全局监听器(watcher saga),用于监听组件发出的action,将监听到的action转发给对应的接收器(worker saga),再由接收器执行具体任务,副作用执行完后,再发出另一个action交由reducer修改state,所以这里必须注意:**watcher saga监听的action和对应worker saga中发出的action不能同名**,否则造成死循环

watcher saga

我们先定义一个watcher saga

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

// watcher saga
function* watchIncrementSaga() {
  yield takeEvery('increment', workIncrementSaga)
}

watcher saga 很简单,就是监听用户派发的action(只用于监听,具体操作交由worker saga),这里使用takeEvery辅助方法,表示每次派发都会被监听到,第一个参数就是用户派发action的类型,第二个参数就是指定交由哪个worker saga进行处理

worker saga

因此我们需要再定义一个名为workIncrementSagaworker saga,我们在里面执行副作用操作,然后使用yield put(...)派发action,让reducer去更新state

import { call, put, takeEvery } from 'redux-saga/effects'

// watcher saga
function* watchIncrementSaga() {
  yield takeEvery('increment', workIncrementSaga)
}

// worker saga
function* workIncrementSaga() {
  function f () {
    return fetch('https://jsonplaceholder.typicode.com/posts').then(res => res.json()).then(data => data)
  }
  const res = yield call(f)
  console.log(res)
  yield put({type: 'INCREMENT'})
}

基本使用就是这样。

上面的代码可能有些难以理解,为什么要用generator函数,callput又是什么方法,下面我们来看看redux-saga里面一些非常重要的概念和API

redux-saga 的世界里,Sagas 都用 Generator 函数实现。我们从 GeneratoryieldJavaScript 对象以表达 Saga 逻辑。 我们称呼那些对象为 EffectEffect 是一个简单的对象,这个对象包含了一些给 middleware 解释执行的信息。 你可以把 Effect 看作是发送给 middleware 的指令以执行某些操作(调用某些异步函数,发起一个 actionstore,等等)。你可以使用 redux-saga/effects 包里提供的函数来创建 Effect

辅助方法(监听类型)

  • takeEvery: 监听类型,同一时间允许多个处理函数同时进行,并发处理
  • takeLatest: 监听类型,同一时间只能有一个处理函数在执行,后面开启的任务会执行,前面的会取消执行
  • takeLeading: 如果当前有一个处理函数正在执行,那么后面开启的任务都不会被执行,直到该任务执行完毕

effect创建器

  • take(pattern)

    watcher saga中使用,用来拦截action,当action匹配到这个take的时候,在发起与 pattern 匹配的 action 之前,Generator 将暂停。实际上就是上面辅助方法的底层实现,例如:

    function* watchDecrementSaga() {
      while(true) {
        yield take('decrement')
        const state = yield select()
        console.log(state, 'state')
        yield put({type: 'DECREMENT'})
      }
    }
    

    此时用户派发一个{type: 'decrement', payload}action,就会被上面的take拦截到,执行相应的代码,然后再去派发一个action,通知reducer修改state,如果没有put,则不会通知reducer修改state,注意需要使用while true一直监听,否则只有第一次派发decrement的action会被拦截,后面的都不会被拦截到。

pattern就是匹配规则,基本有以下几种形式

  1. 如果以空参数或 * 调用 take,那么将匹配所有发起的 action。(例如,take() 将匹配所有 action
  2. 如果它是一个函数,那么将匹配 pattern(action)trueaction。(例如,take(action => action.entities) 将匹配哪些 entities 字段为真的 action
  3. 如果它是一个字符串,那么将匹配 action.type === patternaction。(例如,take(INCREMENT_ASYNC)
  4. 如果它是一个数组,那么数组中的每一项都适用于上述规则 —— 因此它是支持字符串与函数混用的。不过,最常见的用例还属纯字符串数组,其结果是用 action.type 与数组中的每一项相对比。(例如,take([INCREMENT, DECREMENT]) 将匹配 INCREMENTDECREMENT 类型的 action

有了这个规则,我们就可以进行更细粒度的控制拦截到的action,再去做相应的修改。

  • call(fn, ...args)

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

    1. fn: Function - 一个 Generator 函数, 也可以是一个返回 Promise 或任意其它值的普通函数。
    2. args: Array<any> - 传递给 fn 的参数数组。
  • put(action)

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

  • fork(fn, ...args)

    forkcall用法一样,唯一的区别就是fork是非阻塞的,而call是阻塞的

    创建一个 Effect 描述信息,用来命令 middleware 以 非阻塞调用 的形式执行 fn,返回一个 Task 对象。Task对象上有一些实用的方法及属性,比如取消某个网络请求什么的。

    1. fn: Function - 一个 Generator 函数,或返回 Promise 的普通函数
    2. args: Array<any> - 传递给 fn 的参数数组。
  • select(selector, ...args)

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

    获取当前state中的部分数据,第一个参数是一个函数,函数的参数是state,即当前状态,后面的参数依次传递给第一个函数,作为该函数的参数

    function selector (state, index) {
      return state[index]
    }
    
    let state2 = yield select(selector, 0)
    console.log(state2, 'select2');
    

    select 也可以不传任何参数,返回值就直接是当前的所有状态

  • cancel(task)

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

    1. task: Task - 由之前 fork 指令返回的 Task 对象

    cancel 是一个非阻塞的 Effect。也就是说,执行 cancelSaga 会在发起取消动作后立即恢复执行。

    对于返回 Promise 结果的函数,你可以通过给 promise 附加一个 [CANCEL] 来插入自己的取消逻辑。

    举个使用cancel取消请求的例子,

    1. 首先需要从redux-saga库里引入CANCEL(注意不是redux-saga/effects中的)
    2. 然后在异步操作上面自定义一个取消异步操作的函数,需要根据不同的异步操作形式自定义不同的取消函数,下面的例子我们是用fetch进行网络请求的,因此要使用fetch对应的取消请求的方法,如果你用的axios,则需要使用axios取消请求的方法
    3. 把这个取消函数绑定到异步请求上,如下promise[CANCEL] = () => { controller.abort() }
    4. 使用fork去执行异步操作(不会阻塞下面代码执行),返回这个异步操作的task
    5. 如果想要取消这个异步操作,则直接使用redux-saga/effects中的cancel方法取消这个task
    import { call, cancel, put, select, take, takeEvery, fork } from 'redux-saga/effects'
    import {CANCEL} from 'redux-saga'
    
    function* watchChangeName() {
      yield takeEvery('changeName', workerChangeName)
    }
    
    function* workerChangeName({ payload }) {
      function f () {
        const controller = new AbortController();
        const { signal } = controller;
        const promise = fetch('https://jsonplaceholder.typicode.com/posts', { signal }).then(res => res.json()).then(data => console.log(data))
        promise[CANCEL] = () => { controller.abort() }
        console.log(promise)
        return promise
      }
      const fetchTask = yield fork(f)
      yield cancel(fetchTask) // 这里直接调用cancel取消请求
      yield put({type: 'CHANGE_NAME', payload})
    }
    

上面就是我们常用到的一些方法,具体的其他一些用法,参考redux-saga官方文档


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com