Redux-Saga

  • 中文文档, 英文文档

  • 我们之前学习过Redux-promise,Redux-thunk等处理异步操作的Redux中间件, 这里的redux-saga同样是redux的一个中间件, 与前两个不同的是,redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 中间件,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。前两者在redux中的使用都有不同程度的破坏redux中的action(redux-thunk将其变成一个函数, redux-promise将其变成一个promise对象),但是redux-saga不一样,它能非侵入式的结合redux进行开发,即不修改action的前提上实现异步操作。

  • 可以想像为,一个 saga 就像是应用程序中一个单独的线程,它独自负责处理副作用。 redux-saga 是一个redux中间件,意味着这个线程可以通过正常的 redux action 从主应用程序启动,暂停和取消,它能访问完整的 redux state,也可以 dispatch redux action

  • redux-saga 使用了 ES6 Generator 功能,让异步的流程更易于读取,写入和测试。通过这样的方式,这些异步的流程看起来就像是标准同步的 Javascript 代码。有点像 async/await 功能。

实现流程:

image

由图中我们可以看到, saga会监听异步操作的action,一旦有新的异步action就会白它捕获到,随后吐出一个新的完成了异步处理后的action随后返回给reducer再更新到全局的store

实现原理:

  • redux-saga就是使用内置的副作用函数(fork,take,put,call)去捕获带有异步操作的dispatch操作(我们自己定义的字段),随后在saga生成器函数内部使用call去执行异步操作取回数据,再使用put生成一个新的action对象让dispatch去捕获!

基本使用:

  1. 安装
npm i -S redux-saga

案例展示:

  • 模拟异步请求,在页面上设置一个按钮,第一次点击获取数据,第二次点击则获取缓存数据(全局store中的数据)

App.js(组件)

import React, { Component } from 'react'
import store from './redux/store'// 引入全局store

export default class App extends Component {
render() {
return (
<div>
<button onClick={()=>{
if(store.getState().list.length === 0){
// dispatch更新全局数据
store.dispatch({// 请求数据(异步操作!)
type:"get-list"
})
}else{
console.log('使用缓存',store.getState().list);
}
}}>点击获取数据(异步缓存)</button>
</div>
)
}
}

store.js(全局store)

// 创建全局store
import { createStore,applyMiddleware } from "redux";
import reducer from './reducer.js'// 引入reducer
import createSagaMidlleWare from 'redux-saga'// 引入createSagaMidlleWare
import watchSaga from './saga'// 引入watchSaga(监听任务)

const SagaMidlleWare = createSagaMidlleWare()// 创建saga中间件
const store = createStore(reducer,applyMiddleware(SagaMidlleWare))//挂载saga中间件

SagaMidlleWare.run(watchSaga) //运行saga任务

export default store

saga.js(设置异步捕获处理)

import {take,fork,put,call }  from 'redux-saga/effects'// 引入saga操作函数

/*
saga执行流程:
take: 监听带有异步操作的 action
-> fork: 执行异步处理生成器函数
-> call: 执行异步操作获取数据
-> put: 生成新的action(已经完成了异步操作)返回出去给reducer
*/

// 监听异步操作函数(saga入口)
function *watchSaga(){
while(true){// 设置一个死循环函数,持续监听action操作
// take 监听 组件发来的action
yield take("get-list")
// fork 非阻塞调用 的形式执行 fn
yield fork(getList)
}
}

// 异步处理的生成器函数(按照生成器函数流程执行异步操作)
function *getList(){
// call函数发异步请求获取数据
let res = yield call(getListAction)//阻塞的调用fn

// put函数返回新的action(已执行完异步操作),非阻塞式执行
yield put({
type:"change-list",// 新的dispatch字段
payload:res// 异步请求回来的数据
})
}

// 创建一个异步操作函数模拟数据请求
function getListAction(){
return new Promise((resolve,reject)=>{
// 打开定时器模拟数据请求耗时
setTimeout(()=>{
resolve(["数据1","数据2","数据3"])
},1000)
})
}

// 将监听函数(里面包含所有saga操作流程)暴露出去
export default watchSaga

reducer.js(更新全局store中的状态)

// 创建修改函数reducer
function reducer(preState={
list:[],
},action={}){
// 纯函数写法不干扰原状态
var newState = {...preState}
switch(action.type){
// 此时捕获的是saga操作过后的 dispatch 字段: change-list
case "change-list":
console.log('请求到的数据:',action)
newState.list= action.payload
return newState
default:
return preState
}
}

export default reducer

结果展示:

image

当有多个异步操作时使用saga进行监听

  • 原理与上述是一样的,只不过是当有多个异步action需要监听时,我们就设置一个saga文件夹(如下所示),在这个saga文件夹中存这捕获对应dispatch字段的saga操作,随后在saga.js文件中引入all方法,创建一个watchSaga函数去集中管理对应的saga异步操作罢了

image

代码展示:

  1. App.js(组件)
import React, { Component } from 'react'
import store from './redux/store'// 引入全局store

export default class App extends Component {
render() {
return (
<div>
<button onClick={() => {
if (store.getState().list1.length === 0) {
// dispatch更新全局数据
store.dispatch({// 请求数据(异步操作!)
type: "get-list1"
})
} else {
console.log('使用缓存', store.getState().list1);
}
}}>点击获取数据-1</button>

<button onClick={() => {
if (store.getState().list2.length === 0) {
//dispatch
store.dispatch({
type: "get-list2"
})
} else {
console.log("缓存", store.getState().list2)
}

}}>点击获取数据-2</button>
</div>
)
}
}
  1. store.js(全局状态),与上面的代码一致
// 创建全局store
import { createStore,applyMiddleware } from "redux";
import reducer from './reducer.js'// 引入reducer
import createSagaMidlleWare from 'redux-saga'// 引入createSagaMidlleWare
import watchSaga from './saga'// 引入watchSaga(监听任务)

const SagaMidlleWare = createSagaMidlleWare()// 创建saga中间件
const store = createStore(reducer,applyMiddleware(SagaMidlleWare))//挂载saga中间件

SagaMidlleWare.run(watchSaga) //运行saga任务

export default store
  1. saga1.js(无链式操作)[saga文件夹内]
import {take,fork,put,call,takeEvery}  from 'redux-saga/effects'
function *watchSaga1(){
while(true){
// take 监听 组件发来的action
yield take("get-list1")
// fork 非阻塞调用 的形式执行 fn
yield fork(getList1)
}

// 可以使用takeEvery这个高阶函数完全替代上面的整段代码(take 结合 fork)
// yield takeEvery("get-list2",getList2)
}

function *getList1(){
// call函数发异步请求
let res = yield call(getListAction1)//阻塞的调用fn

yield put({
type:"change-list1",
payload:res
})
// put函数发出新的action,非阻塞式执行
}

function getListAction1(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(["数据1","数据2","数据3"])
},2000)
})
}
export default watchSaga1
  1. saga2.js(有链式操作)[saga文件夹内]
import {take,fork,put,call,takeEvery}  from 'redux-saga/effects'
function *watchSaga2(){
while(true){
// take 监听 组件发来的action
yield take("get-list2")
// fork 非阻塞调用 的形式执行 fn
yield fork(getList2)
}

// 可以使用takeEvery这个高阶函数完全替代上面的整段代码(take 结合 fork)
// yield takeEvery("get-list2",getList2)
}

function *getList2(){
//异步处理的,

// call函数发异步请求(链式调用)
/*
call函数的第二个参数可作为第一个参数(异步操作函数)的形参
*/
let res1 = yield call(getListAction2_1)//阻塞的调用fn
let res2 = yield call(getListAction2_2,res1)//阻塞的调用fn

yield put({
type:"change-list2",
payload:res2
})
// put函数发出新的action,非阻塞式执行
}

function getListAction2_1(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(["数据4","数据5","数据6"])
},2000)
})
}

function getListAction2_2(data){
console.log('接收到前一个yield的数据',data);
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve([...data,"数据7","数据8","数据9"])
},2000)
})
}
export default watchSaga2
  1. saga.js(集中管理saga操作)
import {all} from 'redux-saga/effects'
import watchSaga1 from './saga/saga1'// 引入saga操作1
import watchSaga2 from './saga/saga2'// 引入saga操作2

// 集中管理所有的saga操作 (all方法)
function *watchSaga(){
yield all([watchSaga1(),watchSaga2()])
}

export default watchSaga
  1. reducer
// 创建修改函数reducer
function reducer(prevState = {
list1: [],// 状态1
list2: []// 状态2
}, action = {}) {
// 纯函数写法不干扰原状态
var newState = { ...prevState }
switch (action.type) {
// 此时捕获的是saga操作过后的 dispatch 字段: change-list
case "change-list1":
console.log('请求到的数据1:',action)
newState.list1 = action.payload
return newState
case "change-list2":
console.log('请求到的数据2:',action)
newState.list2 = action.payload
return newState
default:
return prevState
}
}

export default reducer

结果展示:

image