Redux-Saga妈妈级教程(下)

2,910 阅读15分钟

此篇文章续 Redux-Saga妈妈级教程(上),上一篇文章触发字数限制,所以开了《Redux-Saga妈妈级教程(下)》。

7,redux-saga提供的取消任务方法,任务并发方法以及任务竞赛方法(cancel,all,race)

cancel方法

前面说到cancel可以取消当前正在执行的任务,但它并不是那么粗暴,当某个任务呗cancel取消时,cancel会给当前被取消任务一个处理取消逻辑的机会,这个机会就是finally。

  • 如下cancel方法代码示例:

    
    // 当前路径:src/LearningSaga/saga/index.js
    
    import { put, take, call, delay, fork, cancel, all, race } from 'redux-saga/effects'
    
    function* forkTask() {
        try {
            // 2.1,延迟2s控制台打印 'forkTask finished'
            yield delay(2000)
            console.log('forkTask finished');
        } catch (error) {
            // 2.2,如果出错则控制台打印 'error'
            console.log('error');
        } finally {
            // 2.3,forkTask不管以怎样形式结束都将执行finally区块内部内容
            console.log('当task以任意方式结束,不管是正常结束,抛错结束,还是当前任务取消结束,finnally都将执行');
        }
    
    }
    function* cancelFork() {
        // 2,cancelFork使用fork启动一个非阻塞的任务forkTask
        const task = yield fork(forkTask)
        // 3,延迟1s
        yield delay(1000)
        // 4,取消任务forkTask
        yield cancel(task)
    }
    
    function* rootSaga() {
        // 1,根saga启动cancelFork任务
        yield call(cancelFork)
    }
    
    export default rootSaga
    
    
    • 首先,根saga启动cancelFork任务

    • cancelFork任务启动后,首先使用fork启动一个非阻塞的任务forkTask,任务forkTask中将延迟2s后输出'forkTask finished'

    • 回到cancelFork任务中,由于yield fork(forkTask)是非阻塞的,所以继续执行下面代码,即 yield delay(1000),阻塞cancelFork任务1s

    • 1s后,此时forkTask还没运行完毕(forkTask内部阻塞2s),cancelFork任务继续执行下一段代码 yield cancel(task),取消forkTask任务

    • 此时forkTask任务被取消,而此时由于forkTask延迟2s的缘故,try中代码并未执行完毕,所以不会执行 console.log('forkTask finished');,但对于cancel而言,终止了forkTask,但还给forkTask提供了处理取消逻辑的地方,就是finally区块,所以finally区块中的 console.log('当task以任意方式结束,不管是正常结束,抛错结束,还是当前任务取消结束,finnally都将执行');将执行。

    • 所以,当我们使用cancel方法取消任务时,如果该任务还有一些取消逻辑,我们可以在finally中完成。

all方法

all方法其实前面有说过,类似Promise.all,为我们提供了任务并发的功能,当all参数中所有任务全部完成,all所在的Generator函数才会恢复执行。而如果参数中某个任务失败且该任务未对错误进行处理,那么错误将冒泡到all所在的Generator中,且取消其他任务。

而all可能是阻塞的也有可能是非阻塞的,这取决于all中创建Effect的形式,如果all参数中使用非阻塞的方法创建任务,那么all就不会阻塞all后面的代码,比如yield all (call(task1),fork(task2)),那么all就会被call(task1)阻塞,如果是yield all(fork(task1),fork(task2)) ,那么all就不会被阻塞。

  • all方法代码示例如下,此处示例中task1中reject,但被task1中错误被catch所处理,所以视为完成,所以该代码演示的是all参数中任务全都完成的场景,具体代码分析按照注释标记顺序看。

    
    // 当前路径:src/LearningSaga/saga/index.js
    
    import { call, delay, all } from 'redux-saga/effects'
    
    function* task1() {
        try {
            // 1.1,这里yield一个失败的Promise,这相当于抛出一个内容为1000的错误
            yield Promise.reject('1000')
            // 1.2,控制台不会输出'task1',因为1.1已经抛出一个错误,所以这段代码不会被执行
            console.log('task1');
        } catch (error) {
            // 1.3,catch捕获 yield Promise.reject('1000') 抛出的错误,所以这里控制台将输出 `task1_error, 1000`
            console.log('task1_error', error)
        } finally {
            // 1.4,task1因为抛错即将结束,结束之前都会执行finally区块中内容,所以控制台将输出'task1_finally'
            console.log('2:task1_finally');
            // 1.5,task1在finally中返回一个'task1 finished'字符串
            return 'task1 finished'
        }
    }
    function* task2() {
        try {
            // 1.6,task2任务延迟2s
            yield delay(2000)
            // 1.7,2s后控制台输出'task2'
            console.log('3:task2');
            // 1.8,task2返回字符串`task2 success`,但是后面finally区块中也有return语句,最终话return值以finally中return为准
            // 但console.log('`task2 success`');依然执行,只不过return结果替换成finally中的return结果
            return console.log('4:task2 success');
        } catch (error) {
            // 1.9,因为task2中没有出现抛错,所以catch不会执行,所以下面代码不会执行
            console.log('1,task2_error', error)
        } finally {
            // 1.91,task任务正常结束,执行finally区块,所以将输出'task2_finally'
            console.log('5:task2_finally');
            // 1.92,finally区块返回字符串'task2 finished',因为finally中return权重大于catch中return
            // 所以最终返回值以finally区块中返回值为准
            return 'task2 finished'
        }
    
    }
    
    function* rootSaga() {
        // 1,根saga使用all并发启动阻塞任务task1与task2
        const res = yield all([call(task1), call(task2)])
        // 2,当all接受到结果时输出结果
        console.log('6:res:', res);
    }
    
    export default rootSaga
    
    
    
    • 下面是上面all代码示例控制台输出效果gif:

      Jun-29-2021 15-33-49.gif

race方法

race方法类似于Promise.race,即race参数中多个任务竞赛,谁先完成,race就结束,这里也分两种情况:1,如果率先完成者正常完成,则取消其他未完成的任务,且完成任务结果时该任务return值,其他取消任务的结果均为undefined。 2,率先完成任务失败(抛错且未处理),则错误冒泡到race所在Generator函数中,且取消其他竞赛中的任务。

  • race方法代码示例如下,此处示例中task1中reject,但被task1中错误catch所处理,所以视为完成,所以该代码演示的是race参数中任务全都完成的场景,具体代码分析按照注释标记顺序看。

    
    // 当前路径:src/LearningSaga/saga/index.js
    
    import { call, delay, race } from 'redux-saga/effects'
    
    function* task1() {
        try {
            // 1.1,这里yield一个失败的Promise,这相当于抛出一个内容为1000的错误
            yield Promise.reject('1000')
            // 1.2,控制台输出'task1',因为1.1已经抛出一个错误,所以这段代码不会被执行
            console.log('task1');
        } catch (error) {
            // 1.3,catch捕获 yield Promise.reject('1000') 抛出的错误,所以这里控制台将输出 `task1_error, 1000`
            console.log('1:task1_error', error)
        } finally {
            // 1.4,task1因为抛错即将结束,结束之前都会执行finally区块中内容,所以控制台将输出'task1_finally'
            console.log('2:task1_finally');
            // 1.5,task1在finally中返回一个'task1 finished'字符串
            return 'task1 finished'
        }
    }
    function* task2() {
        try {
            // 1.6,task2任务延迟2s,因为这里是race,task1明显快于task2,所以task2任务将被取消
            // 所以在finally之前代码都不会执行
            yield delay(2000)
            console.log('task2');
        } catch (error) {
            console.log('task2_error', error)
        } finally {
            // 1.7,task2任务被取消,执行finally区块,所以将输出'task2_finally'
            console.log('3:task2_finally');
            // 1.8,因为task2任务被取消,其return结果不作为返回结果
            return 'task2 finished'
        }
    
    }
    
    function* rootSaga() {
        // 1,根saga使用race竞赛启动阻塞任务task1与task2
        const res = yield race([call(task1), call(task2)])
        // 2,当race接受到结果时输出结果,因为task2被取消,所以其结果都为undefined,不管有没有return
        // 所以输出结果将是: ["task1 finished", undefined]
        console.log('4:res:', res);
    }
    
    export default rootSaga
    
    
    • 下面是上面race代码示例控制台输出效果gif:

      Jun-29-2021 15-54-31.gif

cancel all race方法详解

  • cancel(...tasks): cancel中参数tasks是可选的,如果传参,可以传入一个或多个task,像这样cancel(task)||cancel(task1,task2,...tasks) ,其中task是由fork指令返回的Task对象,用于取消这些task对应的fork分叉任务。其中如果期望在这些fork任务被取消时执行一些取消逻辑可以将这些取消逻辑放在finally区块中。

    • cancel如果没有接受到传参,像这样yield cancel(),将取消该代码所在的任务,即自取消,如下代码,cancelTask任务将被自取消。

      function* cancelTask() {
          try {
              // 1,取消当前任务cancelTask
              yield cancel()
              // 2,由于当前任务被取消,console.log('我还能被执行吗?');将不会执行
              console.log('我还能被执行吗?');
          } catch (error) {
              console.log('error,cancelTask');
          } finally {
              // 3,任务结束最后都会执行finally区块内代码,所以下面代码将执行
              console.log('我肯定可以被执行');
          }
      }
      
  • race([...effects]): 创建一个Effect描述信息,命令中间件在多个任务间竞赛。且分两种情况:1,如果率先完成者正常完成,则取消其他未完成的任务,且完成任务结果时该任务return值,其他取消任务的结果均为undefined。 2,率先完成任务失败(抛错且未处理),则错误冒泡到race所在Generator函数中,且取消其他竞赛中的任务。 前面race演示的为率先完成的任务成功的场景,即情况1,下面演示率先完成任务失败场景(情况2),如下代码,分析过程参注释标记顺序,此处不做赘述。

    // 当前路径:src/LearningSaga/saga/index.js
    
    import { call, delay, all, race } from 'redux-saga/effects'
    
    function* task1() {
        // 1.1,这里yield一个失败的Promise,这相当于reject一个内容为1000的错误
        yield Promise.reject('1000')
        // 1.2,因为1.1已经reject一个错误,所以这段代码不会被执行,同时task1未对错误进行处理,错误将冒泡到父函数
        console.log('task1');
    }
    function* task2() {
        // 1.3,因为task1已经失败,且错误未处理,所以task2任务将取消,后面的 console.log('task2');不会被执行
        yield delay(2000)
        console.log('task2');
    }
    
    function* rootSaga() {
        try {
            // 1,根saga使用race竞赛启动阻塞任务task1与task2
            const res = yield race([call(task1), call(task2)])
            // 2,当race接受到结果时输出结果,但是由于task1中错误未被捕获,所以错误冒泡到rootSaga中,所以下面代码将不会执行
            console.log('6:res:', res);
        } catch (error) {
            // 3,rootSaga的catch捕获到task1,并输出结果
            console.log('1:task1冒泡到rootSaga中的错误,现在已经被rootSaga捕获');
        }
    
    }
    
    export default rootSaga
    
    
    • 上面代码控制台输出gif:

      Jun-29-2021 17-29-10.gif

  • race(effects): 区别于race([...effects]),这里effects是个对象形式的effect集合。其参数形式与输出结果如下code。

    function* raceTask() {
        // 1,task1返回值1000 task2返回值2000 task3返回值3000 
        const res = yield race({
            task1: call(task1),
            task2: call(task2),
            task3: call(task2)
        })
        //
        // 2,其中task2率先完成,所以res结果将是 {task1:undefined,task2:2000,task3:undefined}
        console.log(res);
    }
    
  • all([...effects]) : 创建一个Effect信息,命令中间件并行地运行多个Effect,并等待他们全部完成,其中分为两种情况:1,all中任务全部完成,则all所在Generator恢复执行。 2,all中某个任务失败,则取消其他all中进行的任务,同时错误冒泡到all所在Generator中。

    • 前面all演示例子属于情况1,即all中所有任务全部完成,下面将以code演示情况2,代码如下,分析过程参注释标记顺序,此处不做赘述。

      // 当前路径:src/LearningSaga/saga/index.js
      
      import { call, delay, all } from 'redux-saga/effects'
      
      function* task1() {
          // 1.1,这里yield一个失败的Promise,这相当于reject一个内容为1000的错误
          yield Promise.reject('1000')
          // 1.2,因为1.1已经reject一个错误,所以这段代码不会被执行,同时task1未对错误进行处理,错误将冒泡到父函数
          console.log('task1');
      }
      function* task2() {
          try {
              // 1.3,因为task1已经失败,且错误未处理,所以task2任务将取消,try中代码将不会执行
              yield delay(2000)
              console.log('task2');
          } catch (error) {
      
          } finally {
              // 1.4,task2任务取消,执行finally,所以输出5:task2_finally
              console.log('1:task2_finally');
          }
      }
      
      function* rootSaga() {
          try {
              // 1,根saga使用all并发启动阻塞任务task1与task2
              const res = yield all([call(task1), call(task2)])
              // 2,当all接受到结果时输出结果,但是由于task1中错误未被捕获,所以错误冒泡到rootSaga中,所以下面代码将不会执行
              console.log('6:res:', res);
          } catch (error) {
              // 3,rootSaga的catch捕获到task1,并输出结果
              console.log('2:task1冒泡到rootSaga中的错误,现在已经被rootSaga捕获');
          }
      
      }
      
      export default rootSaga
      
      
    • 上面代码控制台输出gif:

      Jun-29-2021 17-09-48.gif

  • all(effects): 区别于all([...effects]),这里effects是个对象形式的effect集合。其参数形式与输出结果如下code。

      function* allTask() {
        // 1,task1返回值1000 task2返回值2000 task3返回值3000 
        const res = yield all({
            task1: call(task1),
            task2: call(task2),
            task3: call(task2)
        })
        // 2,res结果将是 {task1:1000,task2:2000,task3:3000}
        console.log(res);
      }
    

8,最后我们介绍一个也很常用且简单的redux-saga方法 select

  • select是由redux-saga提供的一个方法,同样的创建一个Effect,命令中间件获取store中的state数据(store即redux中的store)。下面将以代码演示select:

  • 首先看我们的reducer文件内容,代码如下,redux初始化后state数据应该长这样state:{ reducer1:{name:'reducer1'},reducer2:{name:'reducer2'} }

    // 当前路径:src/LearningSaga/reducer/index.js
    
    import { combineReducers } from 'redux'
    
    function reducer1(state = { name: 'reducer1' }, action) {
        return state
    }
    
    function reducer2(state = { name: 'reducer2' }, action) {
        return state
    }
    
    const rootReducer = combineReducers({ reducer1, reducer2 })
    
    export default rootReducer
    
  • 继续看我们的saga文件

    // 当前路径:src/LearningSaga/saga/index.js
    
    import { select } from 'redux-saga/effects'
    
    function* rootSaga() {
        // 1,使用select获取store中state信息
        const state = yield select()
        // 2,输出state信息 
        console.log(state); 
    }
    
    export default rootSaga
    
    
    • 首先const state = yield select(),这里select中没有传入任何参数,意味着即通过select获取到全部store中state数据,于是 console.log(state);将输出全部state数据,如下截图。

      image.png

  • 如果我们想获取state中部分数据,比如我只想获取reducer2数据,那么我们可以向select传入一个选择器函数,告诉select我们想要获取state哪一部分数据,这里以获取reducer2数据举例,代码如下:

    // 当前路径:src/LearningSaga/saga/index.js
    
    import { select } from 'redux-saga/effects'
    
    function* rootSaga() {
        // 1,select通过选择器函数state => state.reducer2获取到state中的reducer2数据
        const reducer2 = yield select(state => state.reducer2)
        // 2,输出reducer2中的数据
        console.log(reducer2);
    }
    
    export default rootSaga
    
    • 在select中传入一个函数state => state.reducer2,函数返回值即yield select(...)最终返回值,该函数中state由redux-saga注入,相当于redux-saga调用redux的getState()方法获取state中数据,将数据交给我们的选择器函数,让我们可以任意选择需要的state中的数据,这里我们需要reducer2数据,所以return了state.reducer2数据,控制台输出结果如下:

      image.png

    • 对于先前的yield select()即select不传参数,其实就是相当于yield select(state=>state),因此能够获取到完整state数据。

  • select的完成参数形式其实是这样  select(selector,...args),其中selector就是前面所说的选择器函数,而args参数即交给选择器的其他参数,选择器默认参数只有一个state,如果存在args,那么选择器参数相当于[state].concat(args),具体效果见下面代码:

    // 当前路径:src/LearningSaga/saga/index.js
    
    import { select } from 'redux-saga/effects'
    
    function* rootSaga() {
        const state = yield select(
             (state, ...args) => { console.log('select除选择器其他的参数都在这:', args); return state }
              , 1, 2, 3
        )
        console.log(state);
    }
    
    export default rootSaga
    
    • 该saga运行控制台输出结果如下:

      image.png

9,fork 与 spawn


// 当前路径:src/LearningSaga/saga/index.js

import { delay, fork, spawn } from 'redux-saga/effects'

function* delayPrint() {
    // 5,延迟1s
    yield delay(1000)
    // 6,输出1,fork任务delayPrint执行完毕,对应的,其父级任务doSth也执行完毕
    console.log('1');
}

function* doSth() {
    // 2:使用fork创建一个分叉任务
    yield fork(delayPrint)
    // 3,因为fork是非阻塞的,所以继续执行 console.log('0'); 输出0
    console.log('0');
    // 4,此时doSth任务不会结束,doSth会等待fork任务完成,才会结束
    // 用官方的话说就是fork的任务(delayPrint)会被附加倒父级任务(doSth)上
    // 所以只有fork任务delayPrint执行完毕时,父级任务才会执行完毕,在此之前,父级任务doSth一直会阻塞
}

function* rootSaga() {
    // 1,启动saga:dosth
    yield doSth()
    // 7,doSth执行完毕,不再阻塞,因此继续执行console.log('2');输出2
    console.log('2');
    // 8,所以,最终控制台输出顺序是0,1,2

}
export default rootSaga

由上可知,fork行为虽然不是阻塞的,但是它创建的分叉任务将附加倒其父级任务上,因此当分叉任务未完成时,即使父任务内其他代码执行玩,也会等待fork任务执行完毕,才会退出父任务,因此,使用fork的函数,是会阻塞的,所以那些使用yield takeEvery 或 yield takeLatest 的函数都会一直阻塞住。(本质yield takeSth() 就是 yield fork(),见前面的使用低阶take实现takeEvery与takeLatest)

如果我们期望非阻塞的创建一个分叉任务,同时该任务不会被附加到父任务上,既该任务是分离的或者独立的,那么替代fork的方法就是spawn,该方法将创建一个完全独立的任务。现在让我们把上面代码中的fork替换成spawn,重新分析一遍执行过程。


// 当前路径:src/LearningSaga/saga/index.js

import { delay, fork, spawn } from 'redux-saga/effects'

function* delayPrint() {
    // 7,delayPrint延迟1s
    yield delay(1000)
    // 8,delayPring输出1,因此控制台输出 0,2,1
    console.log('1');
}

function* doSth() {
    // 2,使用spawn非阻塞的创建一个分叉任务,区别于fork,该任务不会附加到父任务doSth上,是个完全独立的任务
    yield spawn(delayPrint)
    // 3,spawn是非阻塞的,所以执行 console.log('0'); 输出0
    console.log('0');
    // 4,spawn创建的分叉任务不会附加到父任务doSth,因此此时父任务执行完毕,推出
}

function* rootSaga() {
    // 1,启动saga:dosth
    yield doSth()
    // 5,doSth()执行完毕,执行console.log('2');输出2
    console.log('2');
    // 6,此时doSth()中spawn的分叉任务delayPrint任在执行

}

export default rootSaga

结语

至此,已经结合一个个示例,介绍完redux-saga常用方法,如果读者真的完整看下来,这篇文章应该够你入门使用redux-saga了,对于redux-saga一些不常用的方法,以及其他想要了解redux-saga的,可以阅读 redux-saga 官方文档,其实本文基本就是按照官方文档写下来的,同时结合了自己的一些小例子。此外,如果有什么建议或者交流,欢迎留言,我会第一时间回复。