redux处理异步action
redux的action是一个普通的 js 对象,每个action对象都有一个type属性表示将要执行的动作,一个payload属性来携带一些额外的数据。但是action不能是异步的。如果需要处理异步action,redux需要借助中间件来处理
常见处理异步action的中间件有:
- redux-thunk
- redux-saga
在redux里面,中间件是运行在 action 发送出去,到达 reducer修改数据之间的一段代码,就可以把代码调用流程变为 action ->Middlewares ->reducer,这种机制可以改变数据流,实现异步action。
redux-thunk使用
redux-thunk 是一个中间件,允许在action中返回一个函数而不是一个普通的js对象。这个函数接收 dispatch 和 getState 作为参数,可以用来执行异步操作。
参数:
dispatch:类型是一个函数,dispatch函数用于将action发送到 redux的store。调用dispatch(action)会触发store中修改数据的方法来处理这个action,并更新状态。getState:类型是一个函数,getState函数返回当前的 redux的store状态。调用getState()会返回一个包含当前应用状态的对象。
安装:
npm install redux-thunk
在创建store时,注册中间件
// store/index.js
import { legacy_createStore, applyMiddleware } from 'redux'
import { thunk } from 'redux-thunk'
// thunk:这是一个中间件,允许在 action 中返回一个函数而不是一个普通的 action 对象,这个函数可以包含异步操作。
const defaultState = {
singer: "G.E.M.",
album: []
}
const reducer = (state = defaultState, action) => {
switch (action.type) {
case "CHANGESINGER":
state.singer = action.payload;
break;
case "CHANGEALBUM":
state.album = action.payload;
break;
case "RESETSINGER":
state.singer = "G.E.M.";
break;
default:
break;
}
state = JSON.parse(JSON.stringify(state))
return state;
}
// legacy_createStore第一个参数是reducer方法,第二个参数是在创建 store 时应用中间件
let store = legacy_createStore(reducer, applyMiddleware(thunk));
export default store; // 将创建的仓库暴露出去
在组件异步触发dispatch
// main.jsx
import { createRoot } from 'react-dom/client' // 用于渲染页面的
import React from 'react' // 用于创建组件的
import { Provider } from 'react-redux'; // 引入react-redux的Provider组件,然后用这个组件包裹App组件完成react-redux的全局注册
import App from './App.jsx'
import store from './store/index.js';
createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App></App>
</Provider>
)
// App.jsx
import React from 'react';
import { useSelector, useDispatch, useStore } from 'react-redux'
export default function App(props) {
const store = useStore();
// 输出仓库状态
console.log(store.getState(), 'state')
const singer = useSelector((state) => state.singer);
const album = useSelector((state) => state.album);
const dispatch = useDispatch();
// dispatch action修改仓库数据
const changeAlbum = () => {
// 在组件中触发异步动作,模拟网络请求
setTimeout(() => {
// 在action中返回一个函数而不是一个普通的js对象。这个函数接收 dispatch 和 getState 作为参数
dispatch((dispatch, getState) => {
console.log(dispatch, 'dispatch')
console.log(getState, 'getState获取当前状态')
dispatch({ type: 'CHANGEALBUM', payload: ['G.E.M.', '18', 'My Secret', 'Xposed', '新的心跳', '摩天动物园', '启示录'] })
})
}, 1000)
}
return (
<div>
<h1>My App</h1>
<p>歌手:{singer}</p>
<p>专辑:</p>
{
album.length != 0 ? album.map((item, index) => {
return <p key={index}>{item}</p>
}) : <div>暂无数据</div>
}
<button onClick={changeAlbum}>获取数据</button>
</div>
);
}
redux-saga使用
redux-saga 是一个中间件,它使用 ES6的Generator 函数来处理异步逻辑,使得异步代码看起来更像是同步代码,从而更容易理解和维护。
安装:
npm i redux-saga
yarn add redux-saga
常见redux-sagaAPI:
-
MiddlewareAPI:是用于redux关联saga的createSagaMiddleware(options)是创建一个 redux 中间件middleware,并将 saga 连接到 reduxstore通过legacy_createStore创建仓库函数的参数传入,options传递给中间件的选项列表,默认可以不用传递。middleware.run(saga,...args)是动态地运行saga,只能在applyMiddleware阶段之后执行 saga。
-
SagaAPI:是用于监听action的(以下三个都是用于监听action的,只要pattern这个action类型发送过来了,就会触发对应的saga函数的调用)takeEvery(pattern, saga, ...args)是在发起dispatch来修改store仓库数据,并且匹配pattern的每一个action上派生一个saga实例触发action。takeLatest(pattern, saga,...args)是在发起dispatch来修改store仓库数据,并且匹配pattern的每一个action上派生一个saga实例处理action,并自动取消之前所有已经启动但仍在执行中的saga任务throttle(ms, pattern, saga,...args)是在发起dispatch来修改store仓库数据,并且匹配pattern的一个action上派生一个saga实例处理action。它在派生一次任务之后,在指定的毫秒内将暂停派生新的saga任务,这也就是它被命名为节流阀(throttle)的原因
src/store/sagas.js集中写saga,创建一个Saga函数
// store/sagas.js
import { takeEvery, takeLatest, throttle } from 'redux-saga/effects'
// defSaga是一个 Generator 函数
function* defSaga() {
// 第一个参数是匹配dispatch action的关键字
yield takeEvery('takeEvery', function* () {
// 监听takeEvery这个action,就会触发function执行
console.log('takeEvery')
})
yield takeLatest('takeLatest', function* () {
console.log('takeLatest')
})
// 第一个参数传入0毫秒
yield throttle(0, 'throttle', function* () {
console.log('throttle')
})
}
export default defSaga
创建仓库,在redux中使用redux-saga中间件
// store/index.js
import { legacy_createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
// 导入saga,最终需要执行saga
import defSaga from './sagas.js'
const defaultState = {
singer: "G.E.M.",
album: []
}
const reducer = (state = defaultState, action) => {
state = JSON.parse(JSON.stringify(state))
return state;
}
// 使用createSagaMiddleware构建中间件
const sagaMiddleWare = createSagaMiddleware();
// legacy_createStore第一个参数是reducer方法,第二个参数是在创建 store 时应用中间件
let store = legacy_createStore(reducer, applyMiddleware(sagaMiddleWare));
export default store; // 将创建的仓库暴露出去
// 使用中间件执行saga,传递自定义的saga
sagaMiddleWare.run(defSaga)
在React中注册react-redux插件
// main.jsx
import { createRoot } from 'react-dom/client' // 用于渲染页面的
import React from 'react' // 用于创建组件的
import { Provider } from 'react-redux'; // 引入react-redux的Provider组件,然后用这个组件包裹App组件完成react-redux的全局注册
import App from './App.jsx'
import store from './store/index.js';
createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App></App>
</Provider>
)
在组件中派发dispatch
// App.jsx
import React from 'react';
import { useSelector, useDispatch, useStore } from 'react-redux'
export default function App(props) {
const store = useStore();
// 输出仓库状态
console.log(store.getState(), 'state')
const singer = useSelector((state) => state.singer);
const album = useSelector((state) => state.album);
const dispatch = useDispatch();
// dispatch action
const takeEveryeHandle = () => {
dispatch({
type: 'takeEvery'
})
}
const takeLatestHandle = () => {
dispatch({
type: 'takeLatest'
})
}
const throttleHandle = () => {
dispatch({
type: 'throttle'
})
}
return (
<div>
<h1>My App</h1>
<p>歌手:{singer}</p>
<p>专辑:</p>
{
album.length != 0 ? album.map((item, index) => {
return <p key={index}>{item}</p>
}) : <div>暂无数据</div>
}
<button onClick={takeEveryeHandle}>takeEvery</button>
<button onClick={takeLatestHandle}>takeLatest</button>
<button onClick={throttleHandle}>throttle</button>
</div>
);
}
EffectAPI:是用于描述异步操作的行为
select(selector,...args)是获取redux store中的state,如果调用select的参数为空(即yield select()),那么会取得完整的state(与调用redux的getState()的结果相同)。selector是一个选择器函数,...args是传递给选择器函数的参数。
示例:
// store/sagas.js
import { takeEvery, takeLatest, throttle, select } from 'redux-saga/effects'
// defSaga是一个 Generator 函数
function* defSaga() {
// 第一个参数是匹配dispatch action的关键字
yield takeEvery('takeEvery', function* () {
// 监听takeEvery这个action,就会触发function执行
console.log('takeEvery')
// 获取仓库的整个state
const holeState = yield select();
console.log(holeState,'holeState');
})
yield takeLatest('takeLatest', function* () {
console.log('takeLatest')
// 获取仓库state的某个数据
const singer = yield select(state => state.singer);
console.log(singer,'singer');
})
yield throttle(0, 'throttle', function* () {
console.log('throttle')
})
}
export default defSaga
call(fn, ...args)是调用一个函数,并等待其结果。fn是要调用的函数,...args是传递给fn函数的参数。
示例:
// store/sagas.js
import { takeEvery, takeLatest, throttle, select, call } from 'redux-saga/effects'
// defSaga是一个 Generator 函数
function* defSaga() {
// 第一个参数是匹配dispatch action的关键字
yield takeEvery('takeEvery', function* () {
// 监听takeEvery这个action,就会触发function执行
console.log('takeEvery')
// 获取仓库的整个state
const album = yield select(state => state.album);
// 这是一个异步操作,模拟网络请求
const res = yield call((...args) => {
console.log(args,"传入fn函数的参数")
return new Promise((resolve) => {
setTimeout((args) => {
// 模拟获取请求路径和查询 id
console.log(args, '接受fn函数传入的参数args');
resolve(['G.E.M.', '18', 'My Secret', 'Xposed', '新的心跳', '摩天动物园', '启示录']);
}, 1000,args); // 将path, id传递参数给 setTimeout 回调
});
}, '/api/album', 7);
console.log(res, 88);
})
yield takeLatest('takeLatest', function* () {
console.log('takeLatest')
// 获取仓库state的某个数据
const singer = yield select(state => state.singer);
console.log(singer, 'singer');
})
yield throttle(0, 'throttle', function* () {
console.log('throttle')
})
}
export default defSaga
put(action)是派发一个action,参数action是要派发的action对象,它用于修改仓库状态数据的
示例:
saga异步处理action:
// store/sagas.js
import { takeEvery, takeLatest, throttle, call, put } from 'redux-saga/effects'
// defSaga是一个 Generator 函数
function* defSaga() {
// 第一个参数是匹配dispatch action的关键字
yield takeEvery('changeAlbum', function* () {
// 监听takeEvery这个action,就会触发function执行
// 这是一个异步操作,模拟网络请求
const res = yield call((...args) => {
return new Promise((resolve, reject) => {
setTimeout((args) => {
console.log(args);
resolve(['G.E.M.', '18', 'My Secret', 'Xposed', '新的心跳', '摩天动物园', '启示录'])
}, 3000, args)
})
}, '/api/album', 7)
// 拿到异步请求的数据,派发一个action来修改仓库数据
yield put({type:'CHANGEALBUM',payload:res})
})
yield takeLatest('takeLatest', function* () {
console.log('takeLatest')
})
// 第一个参数传入0毫秒
yield throttle(0, 'throttle', function* () {
console.log('throttle')
})
}
export default defSaga
redux创建仓库并配置saga中间件:
// store/index.js
import { legacy_createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
// 导入saga,最终需要执行saga
import defSaga from './sagas.js'
const defaultState = {
singer: "G.E.M.",
album: []
}
const reducer = (state = defaultState, action) => {
switch (action.type) {
case "CHANGEALBUM":
state.album = action.payload;
break;
case "CHANGESINGER":
state.singer = action.payload;
break;
case "RESETSINGER":
state.singer = "G.E.M.";
break;
default:
break;
}
state = JSON.parse(JSON.stringify(state))
return state;
}
// 使用createSagaMiddleware构建中间件
const sagaMiddleWare = createSagaMiddleware();
// legacy_createStore第一个参数是reducer方法,第二个参数是在创建 store 时应用中间件
let store = legacy_createStore(reducer, applyMiddleware(sagaMiddleWare));
export default store; // 将创建的仓库暴露出去
// 使用中间件执行saga,传递自定义的saga
sagaMiddleWare.run(defSaga)
React组件使用仓库数据并修改仓库数据:
// App.jsx
import React from 'react';
import { useSelector, useDispatch, useStore } from 'react-redux'
export default function App(props) {
const store = useStore();
// 输出仓库状态
console.log(store.getState(), 'state')
const singer = useSelector((state) => state.singer);
const album = useSelector((state) => state.album);
const dispatch = useDispatch();
// dispatch action修改仓库数据
const changeAlbum = () => {
dispatch({
type: 'changeAlbum'
})
}
const changeSinger= () => {
dispatch({
type: 'CHANGESINGER',
payload:"G.E.M. 邓紫棋"
})
}
const resetSinger = () => {
dispatch({
type: 'RESETSINGER'
})
}
return (
<div>
<h1>My App</h1>
<p>歌手:{singer}</p>
<p>专辑:</p>
{
album.length != 0 ? album.map((item, index) => {
return <p key={index}>{item}</p>
}) : <div>暂无数据</div>
}
<button onClick={changeSinger}>修改歌手</button>
<button onClick={resetSinger}>重置歌手</button>
<button onClick={changeAlbum}>获取数据</button>
</div>
);
}
take(patternOrChannel)是等待一个特定的action被dispatch,它是一个阻塞的方法。参数patternOrChannel是要监听的action类型或 channel。
示例:
// App.jsx
import React from 'react';
import { useSelector, useDispatch, useStore } from 'react-redux'
export default function App(props) {
const store = useStore();
// 输出仓库状态
console.log(store.getState(), 'state')
const singer = useSelector((state) => state.singer);
const album = useSelector((state) => state.album);
const dispatch = useDispatch();
// dispatch action修改仓库数据
const takeEveryeHandle = () => {
dispatch({
type: 'takeEvery'
})
}
const takeLatestHandle = () => {
dispatch({
type: 'takeLatest'
})
}
const throttleHandle = () => {
dispatch({
type: 'throttle'
})
}
const takeHandle = () => {
dispatch({
type:"take"
})
}
return (
<div>
<h1>My App</h1>
<p>歌手:{singer}</p>
<p>专辑:</p>
{
album.length != 0 ? album.map((item, index) => {
return <p key={index}>{item}</p>
}) : <div>暂无数据</div>
}
{/* <button onClick={change}>change</button>
<button onClick={reset}>reset</button> */}
{/* <button onClick={changeAlbum}>获取数据</button> */}
<button onClick={takeEveryeHandle}>takeEvery</button>
<button onClick={takeLatestHandle}>takeLatest</button>
<button onClick={throttleHandle}>throttle</button>
<button onClick={takeHandle}>take</button>
</div>
);
}
// store/sagas.js
import { take } from 'redux-saga/effects'
// defSaga是一个 Generator 函数
function* defSaga() {
console.log('等待 take这个类型的action被dispatch')
yield take('take')
console.log('take这个类型的action被dispatch')
}
export default defSaga
执行步骤:
- 初始化:当
defSaga被启动时,会输出等待 take这个类型的action被dispatch。 - 等待:Saga 会暂停并等待一个类型为
take的action被dispatch。 - 处理:当
take类型的action被dispatch时,Saga 会继续执行,并输出take这个类型的action被dispatch。 - 结束:输出日志后,
defSaga函数执行完毕,不会继续监听take类型的action。
如果希望每当 take 类型的 action 被 dispatch 时都触发某些逻辑,需要在一个无限循环中监听take,例如使用 while (true) 循环:
// store/sagas.js
import { takeEvery, takeLatest, throttle, take } from 'redux-saga/effects'
// defSaga是一个 Generator 函数
function* defSaga() {
while (true) {
console.log('等待 take这个类型的action被dispatch')
yield take('take')
console.log('take这个类型的action被dispatch')
}
}
export default defSaga
执行步骤:
- 无限循环:
while (true)确保 Saga 会一直监听take类型的action。 - 日志输出:每次循环开始时输出
等待 take这个类型的action被dispatch,表示 Saga 正在等待。 - 暂停等待:
yield take('take')暂停 Saga,等待take类型的action。 - 继续执行:当
take类型的action被dispatch时,输出take这个类型的action被dispatch,然后继续下一次迭代。
更多的其实是使用
takeEvery、takeLatest会监听指定类型的action。
SagaAPI:三个用于监听action的区别
示例:
// store/sagas.js
import { takeEvery, takeLatest, throttle, select, call } from 'redux-saga/effects'
// defSaga是一个 Generator 函数
function* defSaga() {
// 第一个参数是匹配dispatch action的关键字
yield takeEvery('takeEvery', function* () {
// 监听takeEvery这个action,就会触发function执行
// 这是一个异步操作,模拟网络请求
const res = yield call((...args) => {
return new Promise((resolve) => {
setTimeout((args) => {
resolve(['G.E.M.', '18', 'My Secret', 'Xposed', '新的心跳', '摩天动物园', '启示录']);
}, 3000, args); // 将path, id传递参数给 setTimeout 回调
});
}, '/api/album', 7);
console.log("takeEvery:", res);
})
yield takeLatest('takeLatest', function* () {
// 这是一个异步操作,模拟网络请求
const res = yield call((...args) => {
return new Promise((resolve) => {
setTimeout((args) => {
resolve(['G.E.M.', '18', 'My Secret', 'Xposed', '新的心跳', '摩天动物园', '启示录']);
}, 3000, args); // 将path, id传递参数给 setTimeout 回调
});
}, '/api/album', 7);
console.log("takeLatest", res);
})
// 用于计数
let count = 0;
// 第一个参数是指定时间间隔
yield throttle(2000, 'throttle', function* () {
// 执行一次action对应的异步任务,count加1
count++;
// 这是一个异步操作,模拟网络请求
const res = yield call((...args) => {
return new Promise((resolve) => {
setTimeout((args) => {
console.log(args,'args')
resolve(['G.E.M.', '18', 'My Secret', 'Xposed', '新的心跳', '摩天动物园', '启示录']);
}, 3000, args); // 将path, id传递参数给 setTimeout 回调
});
}, '/api/album', 7,count);
console.log("throttle", res);
})
}
export default defSaga
takeEvery作用:监听action,并为每个 action 启动一个新的 saga 实例来处理action。
takeEvery特点:
- 每次
dispatch指定类型的action时,都会启动一个新的saga实例 - 多个
saga实例可以同时运行
点击了几次按钮就
dispatch了几次指定类型的action,就创建了几次saga实例来处理action,所以最后得到的结果就打印了几次。
简而言之,触发了多少次异步的action,就会执行多少次异步的任务。
takeLatest作用:监听action,并启动一个新的 saga 实例来处理action。如果新的action到达,之前的 saga 实例会被取消。
takeLatest特点:
- 只有一个
saga实例在运行。 - 新的
action到达时,会取消当前正在运行的saga实例,并启动新的 saga 实例。
点击了很多次按钮就
dispatch了很多次指定类型的action,但是当新的action到达,之前创建的处理action的 正在运行的saga实例会被取消,所以最后得到的结果是只有最后一次运行的saga实例的结果,所以只打印了一次。
简而言之,每次触发异步的action,会取消掉上一次正在执行的异步任务,已经完成的异步任务不会取消。
throttle作用:监听action,并在指定的时间间隔内最多启动一次 saga 实例来处理,在指定的时间间隔内只会创建一个saga 实例。
throttle特点:
- 在指定的时间间隔内,即使多次
dispatch同一类型的action,也只会启动一次saga实例。 - 时间间隔结束后,如果再次
dispatch同一类型的action,会重新启动saga实例。
简而言之,触发action后,会执行一次对应action的异步任务,那么在第一个参数所指定的毫秒内将不会执行异步任务。时间间隔结束后,触发action还是会执行对应action的异步任务。
示例对比:
假设有一个搜索框,用户每次输入字符都会 dispatch 一个类型为SEARCH_REQUESTED的action :
- 使用
takeEvery:每次用户输入字符时都会发起一个新的请求,可能会导致大量的网络请求。 - 使用
takeLatest:每次用户输入字符时,会取消上一个未完成的请求,并发起新的请求,只处理最新的输入。 - 使用
throttle:每隔一定时间(例如1秒)才会发起一次请求,即使用户快速连续输入字符,也不会频繁发起请求。