Redux 异步操作redux-thunk和redux-saga
在Redux中的action仅支持原始对象(plain object),处理有副作用的action,需要使用中间件。中间件可以在发出action,到reducer函数接受action之间,执行具有副作用的操作,例如网络请求,读取浏览器缓存等。
redux-thunk
redux-thunk是redux的作者提供的一个处理副作用的方案,我们都知道,dispatch必须接收一个原始对象,在里面无法实现副作用逻辑,但是我们可以使用action creator函数,在内部处理副作用,然后返回一个action原始对象。
安装
yarn add redux-thunk
应用redux-thunk中间件
import thunk from 'redux-thunk'
// ...
let store = createStore(reducers, applyMiddleware(thunk))
使用
此时我们不能直接派发action,而是先要创建一个函数,例如下面的changeName,函数签名dispatch和getState,都是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-saga是redux处理副作用的另一种方式,相较于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
因此我们需要再定义一个名为workIncrementSaga的worker 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函数,call,put又是什么方法,下面我们来看看redux-saga里面一些非常重要的概念和API
在 redux-saga 的世界里,Sagas 都用 Generator 函数实现。我们从 Generator 里 yield 纯 JavaScript 对象以表达 Saga 逻辑。 我们称呼那些对象为 Effect。Effect 是一个简单的对象,这个对象包含了一些给 middleware 解释执行的信息。 你可以把 Effect 看作是发送给 middleware 的指令以执行某些操作(调用某些异步函数,发起一个 action 到 store,等等)。你可以使用 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就是匹配规则,基本有以下几种形式
- 如果以空参数或
*调用take,那么将匹配所有发起的action。(例如,take()将匹配所有action) - 如果它是一个函数,那么将匹配
pattern(action)为true的action。(例如,take(action => action.entities)将匹配哪些entities字段为真的action) - 如果它是一个字符串,那么将匹配
action.type === pattern的action。(例如,take(INCREMENT_ASYNC)) - 如果它是一个数组,那么数组中的每一项都适用于上述规则 —— 因此它是支持字符串与函数混用的。不过,最常见的用例还属纯字符串数组,其结果是用
action.type与数组中的每一项相对比。(例如,take([INCREMENT, DECREMENT]) 将匹配INCREMENT或DECREMENT类型的action)
有了这个规则,我们就可以进行更细粒度的控制拦截到的action,再去做相应的修改。
call(fn, ...args)创建一个
Effect描述信息,用来命令middleware以参数args调用函数fn。fn:Function- 一个Generator函数, 也可以是一个返回Promise或任意其它值的普通函数。args:Array<any>- 传递给 fn 的参数数组。
put(action)创建一个
Effect描述信息,用来命令middleware向Store发起一个action。 这个effect是非阻塞型的,并且所有向下游抛出的错误(例如在reducer中),都不会冒泡回到saga当中。fork(fn, ...args)fork和call用法一样,唯一的区别就是fork是非阻塞的,而call是阻塞的创建一个
Effect描述信息,用来命令middleware以 非阻塞调用 的形式执行fn,返回一个Task对象。Task对象上有一些实用的方法及属性,比如取消某个网络请求什么的。fn:Function- 一个 Generator 函数,或返回 Promise 的普通函数args:Array<any>- 传递给 fn 的参数数组。
select(selector, ...args)创建一个
Effect,用来命令middleware在当前Store的state上调用指定的选择器(即返回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取消之前的一个分叉任务。task:Task- 由之前fork指令返回的Task对象
cancel是一个非阻塞的Effect。也就是说,执行cancel的Saga会在发起取消动作后立即恢复执行。对于返回 Promise 结果的函数,你可以通过给 promise 附加一个 [CANCEL] 来插入自己的取消逻辑。
举个使用cancel取消请求的例子,
- 首先需要从
redux-saga库里引入CANCEL(注意不是redux-saga/effects中的) - 然后在异步操作上面自定义一个取消异步操作的函数,需要根据不同的异步操作形式自定义不同的取消函数,下面的例子我们是用
fetch进行网络请求的,因此要使用fetch对应的取消请求的方法,如果你用的axios,则需要使用axios取消请求的方法 - 把这个取消函数绑定到异步请求上,如下
promise[CANCEL] = () => { controller.abort() } - 使用
fork去执行异步操作(不会阻塞下面代码执行),返回这个异步操作的task - 如果想要取消这个异步操作,则直接使用
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