Redux-Saga妈妈级教程(上)

8,294 阅读37分钟

本文将结合示例讲解Redux-Saga基本的使用,同时需要您有一些ES6_Generator/Redux/React-Redux/React相关知识。

1,Redux-Saga简述及项目初始化:

  • 简述:redux-saga是redux的中间件,主要负责从action派发到更新store中间具有副作用行为的处理。

  • 项目初始化:

    • 1,本文基础建立在react应用上,而react应用创建直接使用了react提供的create-react-app方式,像下面这样,直接创建一个react应用,如果有不熟悉该方式创建react,可以阅读 react官方文档教程,非常简单。

      image.png

    • 2,当我们建立起来我们自己的react应用后,就是添加我们的安装包,包括redux,react-redux,react-saga,具体版本见下面package.json截图,其中框起来的就是react应用创建后需要我们手动添加的包,比如安装redux,如果有npm包管理工具,直接npm i redux 即可。

      image.png

    • 3,至此,我们已经搭建了一个简单的react应用,同时安装了我们所需要的包,之后读者就可以根据文章中的示例代码粘贴到自己的项目中运行了,其中每个文件顶部都会有该文件路径注释,如果懒得看路径注释,这里也为读者贴了我自己的项目路径截图,读者根据我的路径截图创建对应文件,

      image.png

2,开始使用Redux-Saga

我们将使用create-react-app创建一个我们自己的react应用,该应用行为即点击按钮,页面上的数据完成+1行为。

  • 下面是react的入口文件,我们使用了redux,react-redux进行状态管理,并使用了redux-saga中间件。

        // 当前路径:src/index.js
    
        // 第三方模块引入
        import React from 'react'  
        import ReactDom from 'react-dom'
        import { Provider } from 'react-redux'
        import { createStore, applyMiddleware } from 'redux'
        import createSagaMiddleware from 'redux-saga'
    
        // 自定义模块引入
        // 1,redux中的 reducer引入
        import rootReducer from './LearningSaga/reducer'
        // 2,react中 Counter组件引入
        import Counter from './LearningSaga/component'
        // 3, redux-saga中间件的 saga文件引入
        import rootSaga from './LearningSaga/saga'
    
        // 4,创建一个redux-saga中间件
        const sagaMiddleware = createSagaMiddleware()
        // 5,将redux-saga中间件加入到redux中
        const store = createStore(rootReducer, {}, applyMiddleware(sagaMiddleware))
        // 6,动态的运行saga,注意 sagaMiddleware.run(rootSaga) 只能在applyMiddleware(sagaMiddleware)之后进行
        sagaMiddleware.run(rootSaga)
    
        // 7,挂载react组件
        ReactDom.render(<Provider store={store}><Counter /></Provider>, document.getElementById('root'))
    
    
  • 注释1是我们要用到的reducer,下面是reducer文件内容,很简单,我们创建了counterReducer,并使用redux提供的combineReducers处理并返回出去,熟悉redux使用的话,这里应该不用太多赘述,就是一个简单的对state.counter进行递增处理的reducer。

    // 当前路径:src/LearningSaga/reducer/index.js
    
    import { combineReducers } from 'redux'
    
    function counterReducer(state = 1, action = {}) {
        switch (action.type) {
            case "increment": return state + 1;
            default: return state
        }
    }
    
    const rootReducer = combineReducers({ counter: counterReducer })
    
    export default rootReducer
    
  • 注释2是我们react挂载的一个Counter组件,下面是Counter组件代码,也是个很简单的组件,其中使用了react-redux对Counter进行处理,这样就能在this.props中获取到redux中的状态以及dispatch方法。

    // 当前路径:src/LearningSaga/component/index.js
    
    import React from 'react'
    import { connect } from 'react-redux'
    
    class Counter extends React.Component {
        // 派发一个type为 increment_saga 的action
        add = () => this.props.dispatch({ type: 'increment_saga' })
        render() {
            return (
                <div>
                    <span>{this.props.counter}</span>
                    <button onClick={this.add}>add1-sync</button>
                </div>
            )
        }
    }
    const mapStateToProps = state => ({ counter: state.counter })
    export default connect(mapStateToProps)(Counter)
    
  • 注释4567完成了对redux创建,redux中redux-saga中间件的使用,以及Counter组件的挂载等。当然这里redux-saga中间件处理方式可能和其它中间件方式略有不同。

  • 在讲注释3之前我们先回顾一下Counter组件的使用,首先Counter组件渲染出store(redux)中counter数据和一个递增按钮add1-sync,点击该按钮将派发一个类型为increment_saga的action。我们期待的事 是该动作派发后,store中的counter数据能够加1,其实如果仅是如此,我们派发一个类型为increment的action就可以做到,因为我们在注释1的reducer中已经实现了对类型为increment的action处理。但我们这里要用redux-saga中间件替我们完成,我们可以想一下redux-saga中间件要完成这个加1行为大概要做怎样一件事,首先,拦截到action,然后做一些处理,最后更新store中的counter数据完成+1。没错,redux-saga中间件的流程就是这样,所以按照这个流程回头看,这里我们派发一个类型为increment_saga的action,redux-saga中间件获取到该action,因为这个行为很纯粹就是+1,redux-saga中间件再继续调用dispatch({type:'increment'})完成对store中counter数据+1。

  • 现在回到注释3,这是redux-saga中saga文件,下面是该文件的代码,我们看到该文件中的函数全是generator函数,没错,redux-saga的世界中,就是通过这些generator函数完成dispatch过程中副作用的处理(当然我们现在这个+1例子中还没涉及到一些强烈具有副作用的行为),这些generator函数我们叫它saga。saga中yield 后面的内容我们称呼它为Effects(redux-saga的任务单元),在Effects中我们可以进行启动其它saga,也可以处理一些副作用操作。

    // 当前路径:src/LearningSaga/saga/index.js
    
    import { all, put, takeEvery } from 'redux-saga/effects'
    
    function* increment() {
        // 相当于:dispatch({ type: 'increment' })
        yield put({ type: 'increment' }) 
    }
    function* watchIncrement() {
        // 监听类型为increment_saga的action,监听到启动increment
        yield takeEvery('increment_saga', increment) 
    }
    
    function* rootSaga() {
        // 启动watchIncrement
        yield all([watchIncrement()])
    }
    export default rootSaga
    
    • 这里我们导出的是一个rootSaga函数,该函数内容很简单,只有一行 yield all([watchIncrement()]),其中all是redux-saga所提供的一个方法,用来启动一个或多个Effects,这里我们只启动了一个watchIncrement的saga。

    • 继续看watchIncrement这个saga,函数内容仅有一行yield takeEvery('increment_saga', increment),其中takeEvery是redux-saga提供的一个方法,该方法传入两个参数(type,saga),其中type即对应action中的type,而saga则是redux-saga中间件匹配到对应type的action时需要启动的saga。所以这行代码的作用很简单,就是监听action类型为increment_saga,并启动对应saga进行处理该action。 现在假设watchIncrement监听到类型为increment_saga的动作,启动increment这个saga进行处理。我们进入increment函数中看看做了什么。

    • increment这个saga中也仅有一行代码 yield put({ type: 'increment' }),这行代码中put也是redux-saga提供的一个方法,其参数为一个action,其作用是产生一个dispatch行为的Effect,其action就是put中的参数action。我们可以理解这个动作就相当于dispatch({ type: 'increment' })。所以这里将派发一个类型为increment动作去更新store中的state。

    • 现在整个saga文件已经介绍完它的作用,我们来复盘从Counter组件点击add1-sync按钮那一刻到最后store中的counter数据加1的流程。

      • 1,react入口文件中注释6启动了rootSaga,rootSaga执行yield all([watchIncrement()]),即启动了watchIncrement这个saga,并等待它运行完成

      • 2,watchIncrement的作用在前面说了,是监听类型为increment_saga的action,如果监听到,则启动increment这个saga对action进行进一步处理。

      • 3,现在Counter组件中add1-sync按钮点击,派发一个类型为increment_saga的动作。

      • 4,watchIncrement这个saga监听到该动作(type:'increment_saga'),启动increment对该action进行处理。

      • 5,increment中通过 yield put({ type: 'increment' }) 派发一个类型为increment的action出去

      • 6,reducer接受到类型为increment的action,执行对应的更新行为,完成store中counter数据+1的过程。

      • 7,最后更新Counter组件中的this.props.counter数据(这个自动更新行为由react-redux替我们完成)。

3,带有副作用的counter数据更新

我们将在原Counter组件 基础上增加一个add1-async按钮,点击该按钮将在1s后对counter数据进行+1

  • 在原Counter组件上新增add1-async按钮

    // 当前路径:src/LearningSaga/component/index.js
    
    import React from 'react'
    import { connect } from 'react-redux'
    
    class Counter extends React.Component {
        add = () => this.props.dispatch({ type: 'increment_saga' })
        // addAsync函数将派发一个类型为incrementAsync_saga的action
        addAsync = () => this.props.dispatch({ type: 'incrementAsync_saga' })
        render() {
            return (
                <div>
                    <span>{this.props.counter}</span>
                    <button onClick={this.add}>add1-sync</button>
                    <button onClick={this.addAsync}>add1-async</button>
                </div>
            )
        }
    }
    const mapStateToProps = state => ({ counter: state.counter })
    export default connect(mapStateToProps)(Counter)
    
  • 我们也要在saga文件中添加incrementAsync_saga动作对应的saga进行处理

    // 当前路径:src/LearningSaga/saga/index.js
    
    import { all, put, takeEvery, delay } from 'redux-saga/effects'
    
    function* increment() {
        yield put({ type: 'increment' }) // 相当于:dispatch({ type: 'increment' })
    }
    function* incrementAsync() {
        // 延迟1s
        yield delay(1000)
        // 1s后,dispatch({ type: 'increment' })
        yield put({ type: 'increment' })
    }
    function* watchIncrement() {
        yield takeEvery('increment_saga', increment) // 监听类型为increment_saga的action,监听到启动increment
    
        // 监听类型为incrementAsync_saga的action,监听到启动incrementAsync
        yield takeEvery('incrementAsync_saga', incrementAsync)
    }
    function* rootSaga() {
        yield all([watchIncrement()]) // 启动watchIncrement
    }
    
    export default rootSaga
    
    • 我们在watchIncrement添加了新的一行代码 yield takeEvery('incrementAsync_saga', incrementAsync),上一节中学习我们知道,这行代码作用是 监听类型为incrementAsync_saga的action,监听到启动incrementAsync。

    • 继续看incrementAsync,相较于之前的increment,多了一行代码 yield delay(1000),其中delay是redux-saga提供的延迟函数,该行代码表示延迟1s后才可以继续处理之后的代码。其本质可以看做这么个函数。

      // delay 相当于这里的 delay_
      function delay_(timeout) {
          return new Promise(r => {
              setTimeout(() => r(), timeout);
          })
      }
      
    • 所以延迟1s后,继续执行 yield put({ type: 'increment' }),即相当于dispatch({ type: 'increment' }) 完成counter更新。

  • 当我们连续点击add1-async按钮时,会出现延迟1s后,counter连续的加1,如果我们期望多次点击中只响应最后一次的dispatch({ type: 'increment_saga' }),我们可以使用redux提供的另一个监听方法takeLatest替换原先的takeEvery,顾名思义takeEvery监听每一次对应action的派发,而takeLatest监听最后一次action的派发,并自动取消之前已经在启动且任在执行的任务。 没错,这个和我们的防抖很类似。下面是替换后的saga文件代码,其他文件代码不变。

    // 当前路径:src/LearningSaga/saga/index.js
    
    import { all, put, takeEvery, delay,takeLatest } from 'redux-saga/effects'
    
    // ... other code
    
    function* watchIncrement() {
        // ... other code
    
        // takeEvery替换成takeLatest,这样如果下一次监听到该action,且上一次 saga任务incrementAsync任在运行,将取消上一次的saga任务,执行本次saga任务incrementAsync
        yield takeLatest('incrementAsync_saga', incrementAsync)
    }
    
    // ... other code
    

4,现在,回顾一下到学习到的redux-saga方法使用及一些名词介绍。

  • takeLatest(pattern,saga,...args): 监听类型为pattern的action的派发,当监听到该类型的action,将执行第二个参数saga,且如果存在上一次已经启动且仍在运行的该saga,takeLatest将取消上一次该saga的运行

    • pattern:takeLatest将监听类型为pattern的action的派发

    • saga:监听到对应action,启动对应saga。

    • args:传递给saga函数的参数。如果takeLatest没有传入args,那么saga函数的参数只有一个,即类型为pattern的action。如果takeLatest传入了其它args参数,那么saga函数的参数将像这样(...args,action)。

      • 如下code,saga文件做了一些修改,其它文件没变,其中takeLatest第一个参数是*,即不再匹配某一个具体的action的type,而是匹配所有的action,现在当我们派发一个类型为increment_saga的action,代码下面是printSagaParams的参数输出结果。

        // 当前路径:src/LearningSaga/saga/index.js
        
        import { all, put, takeEvery, delay, takeLatest } from 'redux-saga/effects'
        // 3,printSagaParams将打印出传入的参数
        function* printSagaParams(...params) {
            console.log('params:', params);
        }
        // 2,监听所有action,执行printSagaParams,且没有其它参数
        function* watchPrintSagaParams() {
            yield takeLatest('*', printSagaParams)
        }
        // 1,启动watchPrintSagaParams
        function* rootSaga() {
            yield all([watchPrintSagaParams()]) 
        }
        
        export default rootSaga
        

        image.png

      • 现在,我们稍微修改watchPrintSagaParams的参数,添加两个参数'hello', 'saga',现在派发一个类型为increment_saga的action,代码下面是printSagaParams的参数输出结果。

        // 当前路径:src/LearningSaga/saga/index.js
        
        import { all, put, takeEvery, delay, takeLatest } from 'redux-saga/effects'
        
        function* printSagaParams(...Params) {
            console.log('Params:', Params);
        }
        function* watchPrintSagaParams() {
            // 新增两个参数 'hello', 'saga'
            yield takeLatest('*', printSagaParams, 'hello', 'saga')
        }
        function* rootSaga() {
            yield all([watchPrintSagaParams()]) // 启动watchPrintSagaParams
        }
        
        export default rootSaga
        

        image.png

  • takeEvery(pattern,saga,...args): 监听类型为pattern的action的派发,当监听到该类型的action,将执行第二个参数saga,且args将作为参数传递给saga函数,与takeLatest唯一不同即 不会取消之前监听到类型pattern的action且正在执行的saga任务。

  • delay(timeout,[val]): 产生一个阻塞的Effect(Effect=>任务单元),阻塞timeout毫秒,并返回val(val非必传)。大白话就是yield delay(1000,'Love U')将阻塞当前代码执行1000ms,并且返回'Love U'

  • put(action): 创建一个Effect,用来命令中间件向store发起该action,这个action是非阻塞的。相当于dispatch(action)

  • all([...effects]): 命令中间件并行的运行多个effects,并等待其全部完成,返回全部effcts结果。相当于Promise.all的行为。一般根saga文件都会使用该方法,即同时启动所有该项目所需要运行的saga任务。

    • 如下代码,我们启动了一个delay(3000, 'Love U')的任务

      // 当前路径:src/LearningSaga/saga/index.js
      
      import { all, put, takeEvery, delay, takeLatest } from 'redux-saga/effects'
      
      function* rootSaga() {
          // 启动delay(3000, 'Love U')任务,因为delay是阻塞任务,所以delayRes将在3s后才能接收到返回的'Love U'值
          const delayRes = yield all([delay(3000, 'Love U')])
          // 3s后输出['Love U']
          console.log('delayRes', delayRes); 
      }
      
      export default rootSaga
      
    • 如下代码,我们同时启动了一个delay任务与一个监听任务watchPrintSagaParams

      // 当前路径:src/LearningSaga/saga/index.js
      
      import { all, put, takeEvery, delay, takeLatest } from 'redux-saga/effects'
      
      function* printSagaParams(...Params) {
          console.log('Params:', Params);
      }
      function* watchPrintSagaParams() {
          yield takeLatest('*', printSagaParams, 'hello', 'saga')
      }
      function* rootSaga() {
          // 并行启动delay任务与监听任务watchPrintSagaParams
          // delay任务将在3s后完成,输出'Love U'
          // watchPrintSagaParams中takeLatest一直处于监听状态,所以watchPrintSagaParams迟迟不会完成
          // 所以在这里我们等不到res的结果输出
          const res = yield all([delay(1000, 'Love U'), watchPrintSagaParams()])
         // 等不到输出结果
         console.log('res', res);
      }
      
      export default rootSaga
      
    • 当使用all并发运行Effects时,中间件将暂停Generator(即上面的rootSaga),直到以下任意情况发生,Generator将继续执行。

      • 所有Effect都成功完成:返回一个包含所有Effect结果的数组,恢复Generator。

      • 在所有Effect完成之前,有任意Effect被reject:Generator中将抛出reject错误。

  • all(effects):与all([...effects])功能相同,只不过传入的是对象不是数组,如下代码,两种写法功能相同。

    function* rootSaga() {
        const res = yield all([delay(1000, 'Love U')])
        // 1s后 输出 ['Love U']
        console.log(res);
    }
    function* rootSaga() {
        const res = yield all({ delayRes: delay(1000, 'Love U') })
        // 1s后 输出 {delayRes:'Love U'}
        console.log(res);
    }
    
  • saga:我们看到的这些generator函数就是saga

  • Effect:Effects是一些简单对象,如下put({ type: 'increment' }),我们使用redux-saga提供的put方法创建一个Effect对象,其输出结果见code下面图片,可以看到put({ type: 'increment' })就是直接创建一个包含一些信息的对象。而对于yield put({ type: 'increment' }),其过程即yield一个Effect(put创建的对象),然后告诉中间件发起一个increment的action。Effects本质是一些简单对象,包含了中间件要执行的指令,当中间件拿到一个被saga yield的Effect,它会暂停当前saga,直到Effect完成,然后saga恢复执行。

    // 当前路径:src/LearningSaga/saga/index.js
    
    import { all, put, takeEvery, delay, takeLatest } from 'redux-saga/effects'
    
    function* rootSaga() {
        // 一般我们这样使用:yield put({ type: 'increment' })
        // 而在这里直接输出 put({ type: 'increment' }) 的结果,看看是什么。
        console.log(`put({ type: 'increment' }):`, put({ type: 'increment' }));
    }
    
    export default rootSaga
    

    image.png

  • 阻塞调用/非阻塞调用:阻塞调用的意思是saga在yield Effect之后会等待其结果返回,结果返回后才会继续执行saga中下一个指令。非阻塞调用的意思是,saga会在yield Effect之后立即恢复执行。下面是一个阻塞调用与非阻塞调用的例子。

    • 阻塞调用:下面代码中call是个会产生阻塞调用的方法,其输出结果见code下面的图片。

      // 当前路径:src/LearningSaga/saga/index.js
      
      import { delay, call } from 'redux-saga/effects'
      
      function* say() {
          console.log(`${new Date().getSeconds()}:Hi ~`);
          yield delay(1000)
          console.log(`${new Date().getSeconds()}:I love u ~`);
          yield delay(1000)
      }
      function* rootSaga() {
          // call方法属于阻塞方法,还没说到这个方法,下面简单介绍一下这个方法
          // 这里yield 一个call Effect,该Effect作用是告诉中间件执行say
          // 由于call属于阻塞调用的方法,所以后面的 console.log 将等待say执行完毕 之后执行 
          yield call(say)
          console.log(`${new Date().getSeconds()}:I love u too !`);
      }
      export default rootSaga
      
      

      image.png

    • 非阻塞调用:fork属于非阻塞调用的方法,其输出结果见code下面的图片。

      // 当前路径:src/LearningSaga/saga/index.js
      
      import { delay, fork } from 'redux-saga/effects'
      
      function* say() {
          console.log(`${new Date().getSeconds()}:Hi ~`);
          yield delay(1000)
          console.log(`${new Date().getSeconds()}:I love u ~`);
          yield delay(1000)
      }
      function* rootSaga() {
          // fork方法属于非方法,还没说到这个方法,下面简单介绍一下这个方法
          // 这里yield 一个fork Effect,该Effect作用是告诉中间件执行say
          // 由于fork属于非阻塞调用,所以先执行say方法,遇到say中的阻塞部分将跳出say,执行 yield fork(say)后面的console.log
          // console.log执行完毕继续回到say中执行
          yield fork(say)
          console.log(`${new Date().getSeconds()}:I love u too !`);
      }
      export default rootSaga
      

      image.png

5,使用redux-saga写一个带有登录注销功能的页面

这次我们要使用redux-saga完成一个可以进行登录注销的简单页面

  • 如下是我们的页面代码

    // 当前路径:src/LearningSaga/component/index.js
    
    import React from 'react'
    import { connect } from 'react-redux'
    
    // 可以进行登录注销功能的页面
    class Home extends React.Component {
        // 1,维护可控input的用户名与密码
        state = { name: 'admin', password: 'admin' }
        // 2,用户名输入更新state中用户名
        nameChange = e => this.setState({ name: e.target.value })
        // 3,密码输入更新state中密码
        passwordChange = e => this.setState({ password: e.target.value })
        // 4,登出,将派发一个类型为loginOut的action
        loginOut = () => this.props.dispatch({ type: 'loginOut' })
        // 5,登录,将派发一个类型为login的action,同时传入参数 用户名与密码
        login = () => this.props.dispatch({
            type: 'login',
            account: {
                name: this.state.name,
                password: this.state.password
            }
        })
    
        render() {
            return (
                <div>
                    {/* 可控用户名input */}
                    <div> 用户名: <input onChange={this.nameChange} value={this.state.name} /></div>
                    {/* 可控密码input */}
                    <div>密码:   <input onChange={this.passwordChange} value={this.state.password} /></div>
                    {/* 登录按钮 */}
                    <button onClick={this.login}>登录</button>
                    {/* 登出按钮 */}
                    <button onClick={this.loginOut}>登出</button>
                    {/* 如果登录成功,将更新redux中的loginInfo.success为true,此时就会显示 xxx 用户登录成功 */}
                    {/* 如果登录失败/未登录,将不显示这段文字 */}
                    {this.props.loginInfo.success ? <div>{this.props.loginInfo.name} 用户登录成功 </div> : null}
                </div>
            )
        }
    }
    // 6,使用connect将组件连接redux的store,并将获取store中登录信息数据加入到this.props中
    const mapStateToProps = state => ({ loginInfo: state.loginInfo })
    export default connect(mapStateToProps)(Home)
    
  • 如下是reducer代码

    // 当前路径:src/LearningSaga/reducer/index.js
    
    import { combineReducers } from 'redux'
    
    // 保存登录之后的信息到store
    function setLoginInfoReducer(state = {}, action = {}) {
        switch (action.type) {
            case "loginSuccess": return { ...state, ...action.loginInfo };
            default: return state
        }
    }
    
    const rootReducer = combineReducers({ loginInfo: setLoginInfoReducer })
    
    export default rootReducer
    
  • 如下是我们调用登录接口代码,这里为了方便直接使用Promise模拟,当用户名与密码都是admin则判定登录成功,否则判定登录失败。

    // 当前路径:src/LearningSaga/service/request.js
    
    export function loginService({ name, password }) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (name === 'admin' && password === 'admin') {
                    resolve({ success: true, name, password })
                } else {
                    reject({ success: false, msg: '账户不合法' })
                }
            }, 1000)
        })
    }
    
  • 最后是我们的saga文件代码

    
    // 当前路径:src/LearningSaga/saga/index.js
    
    import { all, put, take, takeLatest, call } from 'redux-saga/effects'
    import { loginService } from '../service/request'
    
    // 登录功能saga
    function* login(action) {
        try {
            // 调用我们的登录接口,获取登录成功失败的信息
            const loginInfo = yield call(loginService, action.account)
            // 如果登录成功,更新store中的loginInfo
            yield put({ type: 'loginSuccess', loginInfo })
        } catch (error) {
            // 登录失败 弹出登录失败的message
            alert(error.msg)
        }
    }
    // 登出功能saga,更新store中的loginInfo为{ success: false }
    function* loginOut() {
        yield put({ type: 'loginSuccess', loginInfo: { success: false, name: '', password: '' } })
    }
    // watchLogin 监听登录action(type:login)与登出action(type:loginOut),并执行对应的saga
    function* watchLogin() {
        yield takeLatest('login', login)
        yield takeLatest('loginOut', loginOut)
    }
    // rootSaga负责启动watchLogin
    function* rootSaga() {
        yield all([watchLogin()])
    }
    export default rootSaga
    
    
  • 我们的页面最终这个样子

    image.png

  • 详细说明该登录登出应用的工作流程

    • 1,当我们输入用户名密码,点击登录,将派发一个类型为login的action,同时传入参数 用户名与密码

          // 点击登录按钮派发一个类型为login的action
          login = () => this.props.dispatch({
              type: 'login',
              account: {
                  name: this.state.name,
                  password: this.state.password
              }
          })
      
    • 2,saga文件中的watchLogin监听到login的action,启动对应的login的saga处理登录

      function* watchLogin() {
          //监听到login的action 启动login
          yield takeLatest('login', login)
          yield takeLatest('loginOut', loginOut)
      }
      
    • 3,login执行,首先调用loginService接口方法,并传入action中携带的用户名与密码,交给loginService判定登录成功还是失败。

      function* login(action) {
          try {
              // 调用loginService,获取登录成功失败的信息
              const loginInfo = yield call(loginService, action.account)
              yield put({ type: 'loginSuccess', loginInfo })
          } catch (error) {
              alert(error.msg)
          }
      }
      
      • 这里我们用到了一个新的redux-saga提供的方法:call(fn,...args),它将创建一个Effect,用来命令中间件以args参数调用fn,fn可以是一个Generator函数也可以是一个返回Pormise或任意其他值的普通函数。所以这里 const loginInfo = yield call(loginService, action.account) 就是调用loginService,传入参数action.account,获取其结果(如果返回Promise将获得Promise的value)交给 loginInfo。
    • 4,loginService执行,这里loginService模拟调用接口的过程,,如果传入用户名密码验证正确,将在

      export function loginService({ name, password }) {
          return new Promise((resolve, reject) => {
              // 模拟登录接口在1s后返回登录成功还是失败
              setTimeout(() => {
                  // 当用户名密码均为admin时登录成功,返回的loginInfo { success: true, name, password }
                  // 否则返回的loginInfo为{ success: false, msg: '账户不合法' }
                  if (name === 'admin' && password === 'admin') {
                      resolve({ success: true, name, password })
                  } else {
                      reject({ success: false, msg: '账户不合法' })
                  }
              }, 1000)
          })
      }
      
    • 5,回到我们login saga,如果loginService登录失败,将返回一个失败Promise,失败错误将被catch捕获,执行alert(error.msg)

      function* login(action) {
         try {
             // 调用loginService,失败
             const loginInfo = yield call(loginService, action.account)
             yield put({ type: 'loginSuccess', loginInfo })
         } catch (error) {
             // 失败 进入catch 弹出失败信息
             alert(error.msg)
         }
      }
      
    • 6,如果loginService登录成功,将执行yield put({ type: 'loginSuccess', loginInfo }),即派发一个类型为loginSuccess的action,交给reducer更新store中loginInfo为成功的数据

      function* login(action) {
         try {
             // 调用loginService,成功
             const loginInfo = yield call(loginService, action.account)
             // 派发一个类型为loginSuccess的action,更新store中loginInfo
             yield put({ type: 'loginSuccess', loginInfo })
         } catch (error) {
             alert(error.msg)
         }
      }
      
    • 7,登录成功,store中的loginInfo数据将被reducer更新为{ success: true, name:'admin', password:'admin' }

      // 登录成功,更新loginInfo为{ success: true, name:'admin', password:'admin' }
      function setLoginInfoReducer(state = {}, action = {}) {
          switch (action.type) {
              case "loginSuccess": return { ...state, ...action.loginInfo };
              default: return state
          }
      }
      
    • 8,回到登录登出页面中,store中loginInfo被更新为{ success: true, name:'admin', password:'admin' },所以this.props.loginInfo.success为true,此时页面展示 xxx 用户登录成功

      render() {
              return (
                  <div>
                      <div> 用户名: <input onChange={this.nameChange} value={this.state.name} /></div>
                      <div>密码:   <input onChange={this.passwordChange} value={this.state.password} /></div>
                      <button onClick={this.login}>登录</button>
                      <button onClick={this.loginOut}>登出</button>
                      {/*this.props.loginInfo.success 为true,展示 admin 用户登录成功。*/}
                      {this.props.loginInfo.success ? <div>{this.props.loginInfo.name} 用户登录成功 </div> : null}
                  </div>
              )
          }
      }
      
    • 9,登录成功页面效果

      Jun-22-2021 16-33-35.gif

    • 10,最后,我们测试登出按钮,首先派发一个类型为loginOut的action

      loginOut = () => this.props.dispatch({ type: 'loginOut' })
      
    • 11,watchLogin监听到该action,启动登出saga:loginOut

      function* watchLogin() {
          yield takeLatest('login', login)
          // loginOut启动
          yield takeLatest('loginOut', loginOut)
      }
      
    • 12,loginOut启动,派发一个success为false的数据更新store中的loginInfo

      function* loginOut() {
          yield put({ type: 'loginSuccess', loginInfo: { success: false, name: '', password: '' } })
      }
      
    • 13,登出,store中的loginInfo数据将被reducer更新为{ success: false, name:'', password:'' }

      // 登录成功,更新loginInfo为{ success: true, name:'', password:'' }
      function setLoginInfoReducer(state = {}, action = {}) {
          switch (action.type) {
              case "loginSuccess": return { ...state, ...action.loginInfo };
              default: return state
          }
      }
      
    • 14,回到登录登出页面中,store中loginInfo被更新为{ success: false, name:'', password:'' },所以this.props.loginInfo.successfalse,此时页面不展示展示 xxx 用户登录成功

      render() {
              return (
                  <div>
                      <div> 用户名: <input onChange={this.nameChange} value={this.state.name} /></div>
                      <div>密码:   <input onChange={this.passwordChange} value={this.state.password} /></div>
                      <button onClick={this.login}>登录</button>
                      <button onClick={this.loginOut}>登出</button>
                      {/*this.props.loginInfo.success 为false*/}
                      {this.props.loginInfo.success ? <div>{this.props.loginInfo.name} 用户登录成功 </div> : null}
                  </div>
              )
          }
      }
      
    • 15,登出后页面效果

      Jun-22-2021 16-35-09.gif

现在我们在原先的登入登出页面基础上进行一些小改动,首先我们期望登出按钮只有在登录状态下可用,其次,登出按钮用了一次之后就不可用。如下gif演示,登出按钮只有在第一次登录状态下,登出有效,之后再登录,再点击登出按钮无效。

Jun-22-2021 17-02-48.gif

  • 首先让我们的登出按钮只有在登录状态下可用,登入登出页面改动如下,很简单,只是在登出按钮上加上disabled={!this.props.loginInfo.success} 即可完成。

    // 当前路径:src/LearningSaga/component/index.js
    
    import React from 'react'
    import { connect } from 'react-redux'
    
    // 可以进行登录注销功能的页面
    class Home extends React.Component {
        state = { name: 'admin', password: 'admin' }
        nameChange = e => this.setState({ name: e.target.value })
        passwordChange = e => this.setState({ password: e.target.value })
        loginOut = () => this.props.dispatch({ type: 'loginOut' })
        login = () => this.props.dispatch({type: 'login',account: { name: this.state.name, password: this.state.password }})
    
        render() {
            return (
                <div>
                    <div> 用户名: <input onChange={this.nameChange} value={this.state.name} /></div>
                    <div>密码:   <input onChange={this.passwordChange} value={this.state.password} /></div>
                    <button onClick={this.login}>登录</button>
                    {/* 1,登出按钮,disabled只有在this.props.loginInfo.success为true时为false,即登出按钮只有在登录状态下可用 */}
                    <button disabled={!this.props.loginInfo.success} onClick={this.loginOut}>登出</button>
                    {this.props.loginInfo.success ? <div>{this.props.loginInfo.name} 用户登录成功 </div> : null}
                </div>
            )
        }
    }
    const mapStateToProps = state => ({ loginInfo: state.loginInfo })
    export default connect(mapStateToProps)(Home)
    
  • 现在让我们实现登出按钮只能使用一次的功能,代码改动如下,仅改动saga文件中watchLogin的两行代码,即删除原先的yield takeLatest('loginOut', loginOut),同时加上yield take('loginOut')yield call(loginOut)

    
    // 当前路径:src/LearningSaga/saga/index.js
    
    import { all, put, take, takeLatest, call } from 'redux-saga/effects'
    import { loginService } from '../service/request'
    
    function* login(action) {
        try {
            const loginInfo = yield call(loginService, action.account)
            yield put({ type: 'loginSuccess', loginInfo })
        } catch (error) {
            alert(error.msg)
        }
    }
    
    function* loginOut() {
        yield put({ type: 'loginSuccess', loginInfo: { success: false, name: '', password: '' } })
    }
    // 监听登录登出saga
    function* watchLogin() {
        yield takeLatest('login', login)
        // 0,去掉之前使用takeLatest完成的对 类型为loginOut的action的监听
        // yield takeLatest('loginOut', loginOut)
        // 1,使用take等待类型loginOut的action的到来,take将阻塞当前Generator
        yield take('loginOut')
        // 2,take监听到类型loginOut的action,执行yield call(loginOut),即继续登出操作
        yield call(loginOut)
    }
    
    function* rootSaga() {
        yield all([watchLogin()])
    }
    export default rootSaga
    
    • 1,首先观察watchLogin方法,首先执行yield takeLatest('login', login),该方法即创建一个takeLatest的Effect,其作用是监听类型为login的action执行对应的saga,我们之前有说过。而其本质是使用take与fork创建的高阶api,用大白话说就是创建一个一直执行的任务,该任务的功能是监听类型为login的action执行对应的saga。且takeLatest是非阻塞的,即中间件接收到takeLatest创建的Effect之后就去创建一个一直执行的任务,同时继续执行yield takeLatest('login', login)后面的代码。

    • 2,因为takeLatest是非阻塞的,所以中间件创建takeLatest的任务之后继续执行下一行代码,即yield take('loginOut'),这里我们又使用到一个新的redux-saga提供方法take。take接受的参数type即用来匹配action的类型,这里的take作用是创建一个Effect,命令中间件等待指定的action到来(即等待类型为loginOut的action到来),在该action到来之前,将暂停当前Generator,即take是阻塞的,且后面的 yield call(loginOut)在action到来之前是不会执行的。

    • 3,当yield take('loginOut')等待的类型为loginOut的action到来时,Generator开始继续执行后面的代码,即 yield call(loginOut),call将创建一个Effect,命令中间件执行loginOut方法,完成后续的登出操作,因为call方法是阻塞的,所以当前Generator会等待loginOut的完成。当loginOut完成之后,该Generator函数将执行完毕。

    • 4,Generator函数(即watchLogin)将执行完毕退出后,此时只有yield takeLatest('login', login)创建的监听login的任务仍然在后台运行。所以往后再有登出的action派发过来的,并没有任何任务对其保持监听,所以将中间件将忽略掉后续过来的登出action,因此后续的登出操作都将没有对应saga处理,从而实现我们前面期望的登出按钮只能使用一次功能。

在上面代码基础上,我们再加一个小小功能,即在当前页面控制台中输出招聘信息,该招聘信息调用接口返回。

  • 首先实现我们的招聘信息接口,这里同样为了简单直接使用Promise模拟,招聘信息将在5s后返回,招聘接口函数即下面的inviteInfoService方法。

// 当前路径:src/LearningSaga/service/request.js
// 登录接口 
export function loginService({ name, password }) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (name === 'admin' && password === 'admin') {
                resolve({ success: true, name, password })
            } else {
                reject({ success: false, msg: '账户不合法' })
            }
        }, 1000)
    })
}
// 模拟接口获取招聘信息,调用该接口5s后返回招聘信息内容
export function inviteInfoService() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("我们需要100名前端优秀开发工程师,有意请联系微信:Tsuki_")
        }, 5000)
    })
}
  • 其次在我们的saga文件中仅有三处改动,即注释1,2,3三个位置。

    
    // 当前路径:src/LearningSaga/saga/index.js
    
    import { all, put, take, takeLatest, call } from 'redux-saga/effects'
    // 1,引入招聘信息接口获取函数inviteInfoService
    import { loginService, inviteInfoService } from '../service/request'
    
    // 登录功能saga
    function* login(action) {
        try {
            const loginInfo = yield call(loginService, action.account)
            yield put({ type: 'loginSuccess', loginInfo })
        } catch (error) {
            alert(error.msg)
        }
    }
    // 登出功能saga,更新store中的loginInfo为{ success: false }
    function* loginOut() {
        yield put({ type: 'loginSuccess', loginInfo: { success: false, name: '', password: '' } })
    }
    // 2,获取招聘信息并输出至控制台的saga
    function* getInviteInfo() {
        // 2.1,获取招聘信息
        const inviteInfo = yield call(inviteInfoService)
        // 2.2,输出至控制台
        console.log('招人啦:', inviteInfo);
    }
    // 监听登录登出saga
    function* watchLogin() {
        // 3,页面进入时执行getInviteInfo,以获取招聘信息并输出至控制台。
        yield call(getInviteInfo)
    
        yield takeLatest('login', login)
        yield take('loginOut')
        yield call(loginOut)
    }
    // rootSaga负责启动watchLogin
    function* rootSaga() {
        yield all([watchLogin()])
    }
    export default rootSaga
    
    
    • 注释1:引入招聘信息接口函数

    • 注释2:创建一个saga(getInviteInfo),用于获取招聘信息并输出至控制台

    • 注释3:执行getInviteInfo,以获取招聘信息并输出至控制台

  • 下面是加入该功能的页面效果gif展示

    Jun-22-2021 18-29-27.gif

  • 我们发现招聘信息在控制台输出之前,点击登录没有反应,在招聘信息出现之后,点击登录才有反应,所以我们回到watchLogin中看看发生了什么事。

     // ... other code
     // 监听登录登出saga
    function* watchLogin() {
        // 3,页面进入时执行getInviteInfo,以获取招聘信息并输出至控制台。
        yield call(getInviteInfo)
    
        yield takeLatest('login', login)
        yield take('loginOut')
        yield call(loginOut)
    }
    // rootSaga负责启动watchLogin
    function* rootSaga() {
        yield all([watchLogin()])
    }
    export default rootSaga
    
    • 1,首先页面加载完成,rootSaga启动。

    • 2,rootSaga内部继续启动watchLogin

    • 3,watchLogin启动,执行其内部代码,首先遇到获取招聘信息输出至控制台的代码 yield call(getInviteInfo),此时因为call是阻塞调用的,所以在getInviteInfo未完成之前,后面的代码是不会执行的,如下code所示:

      function* watchLogin() {
          // 1,执行yield call(getInviteInfo) ,且call是阻塞调用
          yield call(getInviteInfo)
          // 2,所以在 yield call(getInviteInfo)未完成之前,不会执行下面的代码
          yield takeLatest('login', login)
          yield take('loginOut')
          yield call(loginOut)
      }
      

      所以我们在yield call(getInviteInfo)运行期间,登录监听还没启动,所以此时对于登录没有响应。

  • 现在我们知道登录按钮没有响应是因为 yield call(getInviteInfo)阻塞了,那我们有没有什么好办法解决呢,当然有,使用fork替换call。相比较于call,fork方法将创建一个非阻塞的任务。所以我们将call改成fork。

    function* watchLogin() {
     
        // yield call(getInviteInfo)
        // 将call改成fork,防止阻塞后面代码执行
        yield fork(getInviteInfo)
    
        yield takeLatest('login', login)
        yield take('loginOut')
        yield call(loginOut)
    }
    
  • 下面是yield call(getInviteInfo)切换成 yield fork(getInviteInfo)后的效果。显然fork是有用的。

    Jun-22-2021 18-43-07.gif

  • 现在让我们回顾一下本小节用到的几个新的方法

    • call(fn,...args):call创建一个Effect,用来命令中间件以参数args调用fn,其中fn可以是Generator函数,也可以是一个返回Promise或任意其他值的普通函数,我们以下面testSaga函数配合几种不同的fn函数说明call的行为。

      function* testSaga(){
        const data = yield call(fn,'lov_500')
        console.log(data)  
      }
      
      • 1,当fn是一个Generator函数时,如下code:

         function* fn(msg) {
             yield delay(2000)
             return msg
         }
        
        • 首先 yield call(fn,'lov_500') 中间件调用fn,传入参数'lov_500'

        • 中间件发现fn返回的是个迭代器对象,继续暂停testSaga并执行这个Generator函数

        • 2s后Generator函数返回lov_500,testSaga恢复执行,继续执行console.log(data) 输出lov_500

      • 2,当fn是一个返回Promise的普通函数时,如下code:

        function fn(msg) {
            return new Promise(r => {
                setTimeout(() => {
                    r('hi ~ ')
                }, 2000);
            })
        }
        
        • 首先 yield call(fn,'lov_500') 中间件调用fn,传入参数'lov_500'
        • 中间件发现fn返回的是个Promise实例,继续暂停testSaga直到Promise实例被resolve/reject。
        • 2s后Promise状态更新为成功,value为'hi ~ ',testSaga继续执行console.log(data) 输出hi ~
      • 3,当fn是一个普通函数时,其结果不是迭代器对象也不是Promise实例,如下code:

        function fn(msg) {
            console.log(msg);
            return 'May'
        }
        
        • 首先 yield call(fn,'lov_500') 中间件执行fn,传入参数'lov_500'

        • fn执行 console.log(msg);输出lov_500,之后return一个字符串May

        • 中间件发现fn结果既不是迭代器对象也不是Promise,则将结果立即返回给testSaga。

        • testSaga获取到fn返回的字符串May,继续执行console.log(data) 输出May

      • 综上,call创建的Effect会命令中间件调用传入的函数,并检查其结果,如果结果是迭代器对象或者是Promise实例中间件将一直暂停当前saga直到迭代器对象或Promise实例处理完毕。如果返回结果不是迭代器对象,则中间件会立即把该值交给call所在的saga,从而让saga可以以同步的形式恢复执行。

    • call( [context,fn] , ...args):相当于call(fn,...args),但fn上下文是context。

    • call( [context,fnName] , ...args):fnName是个字符串,功能类似于call(context[fnName], ...args)。

    • take(pattern):take创建一个Effect,命令中间件等待指定action,在与pattern匹配action到来之前,当前take所在的Generator函数将暂停。所以这是个阻塞调用的方法。

      • 中间件(redux-saga)还提供了一种特殊的action END,如果发起END action,那么无论那种pattern,只要是被take Effect阻塞的saga都会被终止。如果被终止的saga下任有分叉任务还在运行,则该saga终止前,会等待其所有子任务均被终止。
    • fork(fn, ...args) : fork创建一个Effect,命令中间件以非阻塞的形式调用fn,且返回一个task对象,类似非阻塞形式的call。fork表现形式为创建一个分叉的task去执行fn(怎么像多线程),且fork所在的saga不会在等待fn返回结果的时候被中间件暂停,相反,它在fn被调用时便会立即恢复执行。

6,使用低阶take实现takeEvery与takeLatest

这一小节我们将使用take,fork实现redux-saga提供的takeEvery与takeLatest。

实现takeEvery:

回顾一下takeEvery(pattern,saga,...args)作用,监听一个类型为pattern的action,监听到则启动saga,如果没有args参数,saga参数默认为监听到的action,如果有则为args.concat(action)

  • 首先看我们的新的react组件代码:当我们点击按钮获取前端招聘信息获取后端招聘信息将派发一个类型为getinfo的action,同时参数language分别为javascript与java。以按钮获取前端招聘信息举例,当点击该按钮之后,1s后按钮下方将展示招聘信息:我们需要100名前端优秀javascript开发工程师,有意请联系微信:Tsuki_

    // 当前路径:src/LearningSaga/component/index.js
    
    import React from 'react'
    import { connect } from 'react-redux'
    
    // 点击该页面的两个按钮获取不同的招聘信息,且招聘信息将展示在页面下方
    class InviteInfoPage extends React.Component {
        // 1,点击按钮 获取前端招聘信息 将派发一个类型为 getinfo 的action,同时传入参数language:javascript
        // 2,点击按钮 获取后端招聘信息 将派发一个类型为 getinfo 的action,同时传入参数language:java
        getInfo = language => this.props.dispatch({ type: 'getinfo', language })
        render() {
            return (
                <div>
                    <button onClick={this.getInfo.bind(this, 'javascript')}>获取前端招聘信息:</button>
                    <button onClick={this.getInfo.bind(this, 'java')}>获取后端招聘信息:</button>
                    {/* 3,获取store中inviteInfo信息展示 */}
                    <div>{this.props.inviteInfo}</div>
                </div>
            )
        }
    }
    const mapStateToProps = state => ({ inviteInfo: state.inviteInfo })
    export default connect(mapStateToProps)(InviteInfoPage)
    
  • 在看我们的新reducer文件,也很简单

    // 当前路径:src/LearningSaga/reducer/index.js
    
    import { combineReducers } from 'redux'
    
    // 接受到类型为setInviteInfo的action,将action中参数inviteInfo更新到store中的inviteInfo中
    function setInviteInfoReducer(state = '', action = {}) {
        switch (action.type) {
            case "setInviteInfo": return action.inviteInfo
            default: return state
        }
    }
    
    const rootReducer = combineReducers({ inviteInfo: setInviteInfoReducer })
    
    export default rootReducer
    
  • 看我们的接口调用函数,这里依旧使用Promise模仿接口调用,如下,1s后inviteInfoService返回字符串数据 我们需要100名前端优秀${language}开发工程师,有意请联系微信:Tsuki_,即我们组件页面将要展示的招聘信息。

    // 模拟接口获取招聘信息,即1s后该接口返回招聘信息
    export function inviteInfoService(language) {
        return new Promise((resolve, reject) => {
            setTimeout(() => resolve(`我们需要100名前端优秀${language}开发工程师,有意请联系微信:Tsuki_`), 1000)
        })
    }
    
  • 最后看saga文件,如何自己实现一个takeEvery

    
    // 当前路径:src/LearningSaga/saga/index.js
    
    import { put, take, call, fork } from 'redux-saga/effects'
    
    import { inviteInfoService } from '../service/request'
    
    // 2.1,实现takeEvery,takeEvery接受三个参数:
    // pattern:监听的action的type
    // saga:监听到当前action所需要执行的saga函数
    // args:其他交给takeEvery的参数
    function takeEvery(pattern, saga, ...args) {
        function* help() {
            while (true) {
                const action = yield take(pattern)
                yield fork(saga, ...args.concat(action))
            }
        }
        return fork(help)
    }
    // 3,获取招聘信息接口数据并更新到store中
    function* getInfo(action) {
        // 3.1,getInfo使用call调用接口函数inviteInfoService,同时传入action中的参数language
        const inviteInfo = yield call(inviteInfoService, action.language)
        // 3.2,获取到接口返回的招聘信息数据,使用put派发类型为setInviteInfo的action将招聘信息inviteInfo更新到store中
        yield put({ type: 'setInviteInfo', inviteInfo })
    }
    // 2,使用takeEvery(这里takeEvery我们将自己实现)监听类型为getinfo的action,监听到执行getInfo
    function* watchGetInfo() {
        yield takeEvery('getinfo', getInfo)
    }
    // 1,根saga,启动watchGetInfo
    function* rootSaga() {
        yield watchGetInfo()
    }
    
    export default rootSaga
    
    
  • 现在让我们梳理一下如何使用自制的takeSaga完成的点击获取招聘信息按钮梳理到招聘信息展示的整个流程。

    • 1,首先react代码运行,react入口js文件中的redux-saga中间件运行rootSaga,即 sagaMiddleware.run(rootSaga)

      // 当前路径:src/index.js
      // react 入口js文件
      
      // ... other code ...
      
      // 1,创建redux,并使用redux-saga中间件
      const sagaMiddleware = createSagaMiddleware()
      const store = createStore(rootReducer, {}, applyMiddleware(sagaMiddleware))
      // 2,redux-saga中间件启动根saga
      sagaMiddleware.run(rootSaga)
      
      // ... other code ...
      
      
    • 2.1,回到saga文件,rootSaga启动,代码如下,即运行watchGetInfo

      // 根saga,启动watchGetInfo
      function* rootSaga() {
          yield watchGetInfo()
      }
      
    • 2.2,watchGetInfo启动,代码如下,使用我们自制的watchGetInfo监听类型为getinfo的action,监听到执行getInfo

      // 使用takeEvery(这里takeEvery我们将自己实现)监听类型为getinfo的action,监听到执行getInfo
      function* watchGetInfo() {
          yield takeEvery('getinfo', getInfo)
      }
      
    • 2.3,实现takeEvery的具体代码,如下,这里的takeEvery接受三个参数,分别是pattern, saga, args,对应到当前场景这三个参数值分别是action类型getinfo,saga函数getInfo,以及剩余参数[](这里没给其他参数,所以剩余参数为空)

      function takeEvery(pattern, saga, ...args) {
          function* help() {
              while (true) {
                  const action = yield take(pattern)
                  yield fork(saga, ...args.concat(action))
              }
          }
          return fork(help)
      }
      
    • 2.3.1,takeEvery中首先声明了一个generator函数help,函数体是个无限循环 while (true) { }

      function* help() {
          while (true) {
              const action = yield take(pattern)
              yield fork(saga, ...args.concat(action))
          }
      }
      
      • ,继续分析while中代码,首先 const action = yield take(pattern),其中pattern对应到当前具体例子就是getinfo,所以这行代码的含义是等待一个类型为getinfo的action,在到来之前暂停当前generator函数help

      • 当类型为getinfo的action到来,恢复当前generator函数help执行,并将getinfo的action赋值给action

      • 此时启动一个非阻塞的分叉任务yield fork(saga, ...args.concat(action)),其中args为空,action为getinfo的action,所以 ...args.concat(action)的结果就是[action],而其saga对应到当前例子,就是saga函数getInfo。所以yield fork(saga, ...args.concat(action))的含义即启动一个非阻塞任务,调用saga函数getInfo,并传入参数[action]

      • 因为整个help函数是在一个无限循环体中,所以 yield fork(saga, ...args.concat(action))启动完又继续执行 const action = yield take(pattern),即等待下一个类型为getinfo的action,在到来之前暂停当前generator函数help。一直重复该过程。

    • 2.3.2,介绍完函数help,此时takeEvery只剩下一段代码yield fork(help),即非阻塞的形式启动help。

    • 2.3.3,到这里takeEvery已经实现完毕,我们使用了低阶api:take,配合while循环与fork完成。我们再一次梳理以下takeEvery实现过程。

      • 首先takeEvery作用即监听action并执行对应的saga函数,且takeEvery是非阻塞的。

      • 我们使用take等待该action到来,一旦该action 到来使用fork运行该action对应的处理saga函数,同时继续使用take等待该下一个action到来,重复该过程。实现takeEvery对某action的监听。

    • 3,saga文件内的函数至此运行完毕,现在我们点击组件按钮 获取前端招聘信息: 派发一个类型为getinfo的action,同时传入参数language:javascript,具体代码如下:

         // ... other code ...
         getInfo = language => this.props.dispatch({ type: 'getinfo', language })
         // ... other code ...
         <button onClick={this.getInfo.bind(this, 'javascript')}>获取前端招聘信息:</button>
         // ... other code ...
      
    • 4,saga文件中takeEvery函数如下, const action = yield take(pattern)现在等到了该action并使用fork启动了saga函数getInfo,并传入参数[action]

      function* takeEvery(pattern, saga, ...args) {
          function* help() {
              while (true) {
                  const action = yield take(pattern)
                  yield fork(saga, ...args.concat(action))
              }
          }
          yield fork(help)
      }
      
    • 5,继续执行saga函数getInfo,代码如下,很简单,调用inviteInfoService获取对应language的招聘数据,获取完毕派发一个类型为setInviteInfo的action,同时action携带inviteInfo即接口返回的招聘信息数据。

      function* getInfo(action) {
          // getInfo使用call调用接口函数inviteInfoService,同时传入action中的参数language
          const inviteInfo = yield call(inviteInfoService, action.language)
          // 获取到接口返回的招聘信息数据,使用put派发类型为setInviteInfo的action将招聘信息inviteInfo更新到store中
          yield put({ type: 'setInviteInfo', inviteInfo })
      }
      
    • 6,setInviteInfoReducer接收到类型为setInviteInfo的action将store中的inviteInfo数据完成更新。

          function setInviteInfoReducer(state = '', action = {}) {
              switch (action.type) {
                  case "setInviteInfo": return action.inviteInfo
                  default: return state
              }
          }
      
    • 7,最后我们的组件中props数据通过react-redux更新触发组件重新渲染,this.props.inviteInfo的数据将是我们的招聘信息数据,展示在页面上,整个流程效果如下gif:

      Jun-28-2021 16-46-19.gif

继续实现takeLatest:

回顾一下takeLatest(pattern,saga,...args)作用,其实和takeEvery类似,区别在于,如果多个action监听到来,takeLatest会取消之前未完成的saga,只执行最后一个saga。

  • takeLatest实现,代码如下,这里有两个陌生的东西,lastTask与redux-saga提供的cancel方法,下面我们将逐一分析。

    import { put, take, call, fork, cancel } from 'redux-saga/effects'
    
    function takeLatest(pattern, saga, ...args) {
        // 0,声明lastTask
        let lastTask = null
        function* help() {
            // 2,类似takeEvery的help,这是个无限循环函数体
            while (true) {
                // 3,暂停help,等待类型为pattern的action到来
                const action = yield take(pattern)
                // 4,如果有lastTask先取消lastTask对应的fork任务
                if (lastTask) yield cancel(lastTask)
                // 5,执行fork任务,并将返回的task保存给lastTask
                lastTask = yield fork(saga, ...args.concat(action))
            }
        }
        // 1,非阻塞的启动help函数
        return fork(help)
    }
    
    • 1,分析代码之前,我们回顾一下fork,fork创建一个非阻塞的任务,同时拥有一个返回值task,代码表示const task = yield fork(saga,...args),task是一个具备着某些实用方法与属性的对象。一个task就想一个在后台运行的进程,在redux-saga应用程序中,可以运行多个task,task可通过fork函数创建。

    • 2,redux-saga提供的cancel方法:cancel(task)用于取消一个fork创建的任务,参数task即该fork返回的task对象。

    • 3,介绍了cancel方法与task,现在开始分析代码。

    • 3.1,首先fork启动help函数,help函数又一个while循环

    • 3.2,while循环内 const action = yield take(pattern)即暂停help等待类型为pattern的action到来。

    • 3.3, if (lastTask) yield cancel(lastTask)即如果有lastTask,则取消该task

    • 3.4,lastTask = yield fork(saga, ...args.concat(action)),fork创建一个任务用来处理类型为pattern的action,并将fork返回的task保存到lastTask中

    • 3.5,综合3.3,3.4,我们可以看到如果连续到来类型为pattern的action,help将取消上一次的fork任务,仅执行最新的fork任务,即实现了takeLates只执行最后一次的对应action的处理saga。

  • 现在我们在之前saga文件中修改以下takeInfo这个saga函数,很简单,只是在原有基础上加上 console.log('action', action);,如果我们实用takeEvery连续点击获取招聘信息,控制台将连续输出action信息,而实用takeLatest将只输出最后一次action信息。

    // 3,获取招聘信息接口数据并更新到store中
    function* getInfo(action) {
        console.log('action', action);
        // 3.1,getInfo使用call调用接口函数inviteInfoService,同时传入action中的参数language
        const inviteInfo = yield call(inviteInfoService, action.language)
        // 3.x,测试takeLatest是否可以只执行最后一次的getInfo
        console.log('action', action);
        // 3.2,获取到接口返回的招聘信息数据,使用put派发类型为setInviteInfo的action将招聘信息inviteInfo更新到store中
        yield put({ type: 'setInviteInfo', inviteInfo })
    }
    
  • takeEvery效果展示:可以看到连续输出了action信息

    Jun-28-2021 17-36-18.gif

  • takeLatest效果展示:可以看到只输出一次action信息

    Jun-28-2021 17-35-17.gif

  • takeEvery与takeLatest为什么是非阻塞的?

    回答这个问题之前先以takeEvery代码举例,如下,可以看到,takeEvery中其实执行的仅仅是 yield fork(help),虽然说help函数内部有一个无限循环体,而且while一会暂停一会启动的,但是这不会影响到我们其他代码。因为对于fork,我们可理解为fork创建一个单独的进程,所以help运行在单独进程中,这不会影响到主进程里面的代码运行,所以不管help怎么折腾,都不会影响takeEvery之外的代码,takeEvery可以看做fork一个进程运行help就结束了,所以takeEvery与takeLatest是非阻塞的。

    function* takeEvery(pattern, saga, ...args) {
        function* help() {
            while (true) {
                const action = yield take(pattern)
                yield fork(saga, ...args.concat(action))
            }
        }
        yield fork(help)
    }
    

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

由于本文触发字数限制,后面的内容在 Redux-Saga妈妈级教程(下) :)。