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