redux处理异步action

312 阅读13分钟

redux处理异步action

redux的action是一个普通的 js 对象,每个action对象都有一个type属性表示将要执行的动作,一个payload属性来携带一些额外的数据。但是action不能是异步的。如果需要处理异步action,redux需要借助中间件来处理

常见处理异步action的中间件有:

  1. redux-thunk
  2. redux-saga

在redux里面,中间件是运行在 action 发送出去,到达 reducer修改数据之间的一段代码,就可以把代码调用流程变为 action ->Middlewares ->reducer,这种机制可以改变数据流,实现异步action

redux-thunk使用

redux-thunk 是一个中间件,允许在action中返回一个函数而不是一个普通的js对象。这个函数接收 dispatchgetState 作为参数,可以用来执行异步操作。

参数:

  • 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>
  );
}

2.gif

redux-saga使用

redux-saga 是一个中间件,它使用 ES6的Generator 函数来处理异步逻辑,使得异步代码看起来更像是同步代码,从而更容易理解和维护。

安装:

npm i redux-saga
yarn add redux-saga

常见redux-sagaAPI:

  1. MiddlewareAPI:是用于redux关联saga的

    • createSagaMiddleware(options)是创建一个 redux 中间件middleware,并将 saga 连接到 redux store通过 legacy_createStore 创建仓库函数的参数传入,options传递给中间件的选项列表,默认可以不用传递。
    • middleware.run(saga,...args)是动态地运行saga,只能在 applyMiddleware 阶段之后执行 saga。
  2. 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>
  );
}

2.gif

  1. 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

2.gif

  • 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

2.gif

  • 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>
  );
}

4.gif

  • take(patternOrChannel)是等待一个特定的 actiondispatch,它是一个阻塞的方法。参数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

5.gif

执行步骤:

  1. 初始化:当 defSaga 被启动时,会输出 等待 take这个类型的action被dispatch
  2. 等待:Saga 会暂停并等待一个类型为 takeactiondispatch
  3. 处理:当 take 类型的 actiondispatch 时,Saga 会继续执行,并输出 take这个类型的action被dispatch
  4. 结束:输出日志后,defSaga 函数执行完毕,不会继续监听 take 类型的 action

如果希望每当 take 类型的 actiondispatch 时都触发某些逻辑,需要在一个无限循环中监听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

3.gif

执行步骤:

  1. 无限循环:while (true) 确保 Saga 会一直监听 take 类型的 action
  2. 日志输出:每次循环开始时输出 等待 take这个类型的action被dispatch,表示 Saga 正在等待。
  3. 暂停等待:yield take('take') 暂停 Saga,等待 take 类型的 action
  4. 继续执行:当 take 类型的 actiondispatch 时,输出 take这个类型的action被dispatch,然后继续下一次迭代。

更多的其实是使用takeEverytakeLatest 会监听指定类型的 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 实例可以同时运行

2.gif

点击了几次按钮就dispatch 了几次指定类型的 action ,就创建了几次 saga 实例来处理action,所以最后得到的结果就打印了几次。

image.png

简而言之,触发了多少次异步的action,就会执行多少次异步的任务。

takeLatest作用:监听action,并启动一个新的 saga 实例来处理action。如果新的action到达,之前的 saga 实例会被取消。

takeLatest特点:

  • 只有一个 saga 实例在运行。
  • 新的 action 到达时,会取消当前正在运行的 saga 实例,并启动新的 saga 实例。

3.gif

点击了很多次按钮就dispatch 了很多次指定类型的 action ,但是当新的action到达,之前创建的处理action的 正在运行的saga 实例会被取消,所以最后得到的结果是只有最后一次运行的saga 实例的结果,所以只打印了一次。

image.png

简而言之,每次触发异步的action,会取消掉上一次正在执行的异步任务,已经完成的异步任务不会取消。

throttle作用:监听action,并在指定的时间间隔内最多启动一次 saga 实例来处理,在指定的时间间隔内只会创建一个saga 实例。

throttle特点:

  • 在指定的时间间隔内,即使多次 dispatch 同一类型的action,也只会启动一次 saga 实例。
  • 时间间隔结束后,如果再次 dispatch 同一类型的 action,会重新启动 saga 实例。

4.gif

image.png

简而言之,触发action后,会执行一次对应action的异步任务,那么在第一个参数所指定的毫秒内将不会执行异步任务。时间间隔结束后,触发action还是会执行对应action的异步任务。

示例对比: 假设有一个搜索框,用户每次输入字符都会 dispatch 一个类型为SEARCH_REQUESTEDaction

  • 使用 takeEvery:每次用户输入字符时都会发起一个新的请求,可能会导致大量的网络请求。
  • 使用 takeLatest:每次用户输入字符时,会取消上一个未完成的请求,并发起新的请求,只处理最新的输入。
  • 使用 throttle:每隔一定时间(例如1秒)才会发起一次请求,即使用户快速连续输入字符,也不会频繁发起请求。