zoukankan      html  css  js  c++  java
  • React redux-saga使用详解

    前言

    React的作用View层次的前端框架,自然少不了很多中间件(Redux Middleware)做数据处理, 而redux-saga就是其中之一,目前这个中间件在网上的资料还是比较少,估计应用的不是很广泛,但是如果使用得当,将会事半功倍的效果,下面仔细介绍一个这个中间件的具体使用流程和应用场景。

    redux-saga简介

    Redux-saga是Redux的一个中间件,主要集中处理react架构中的异步处理工作,被定义为generator(ES6)的形式,采用监听的形式进行工作。

    redux-saga安装

    使用npm进行安装:

    npm install --save redux-saga

    或者使用yarn:

    yarn add redux-saga

    redux-saga常用方法解释

    redux Effects

    Effect 是一个 javascript 对象,可以通过 yield 传达给 sagaMiddleware 进行执行在, 如果我们应用redux-saga,所有的 Effect 都必须被 yield 才会执行。

    举个例子,我们要改写下面这行代码:

    yield fetch(url);

    应用saga:

    yield call(fetch, url)

    take

    等待 dispatch 匹配某个 action 。

    比如下面这个例子:

    ....
    while (true) {
      yield take('CLICK_Action');
      yield fork(clickButtonSaga);
    }
    ....

    put

    触发某个action, 作用和dispatch相同:

    yield put({ type: 'CLICK' });

    具体的例子:

    import { call, put } from 'redux-saga/effects'
    
    export function* fetchData(action) {
       try {
          const data = yield call(Api.fetchUser, action.payload.url)
          yield put({type: "FETCH_SUCCEEDED", data})
       } catch (error) {
          yield put({type: "FETCH_FAILED", error})
       }
    }

    select

    作用和 redux thunk 中的 getState 相同。通常会与reselect库配合使用。

    call

    有阻塞地调用 saga 或者返回 promise 的函数,只在触发某个动作。

    takeEvery

    循环监听某个触发动作,我们通常会使用while循环替代。

    import { takeEvery } from 'redux-saga/effects'
    
    function* watchFetchData() {
      yield takeEvery('FETCH_REQUESTED', fetchData)
    }

    takeLatest

    对于触发多个action的时候,只执行最后一个,其他的会自动取消。

    import { takeLatest } from 'redux-saga/effects'
    
    function* watchFetchData() {
      yield takeLatest('FETCH_REQUESTED', fetchData)
    }

    fork 和 cancel

    通常fork 和 cancel配合使用, 实现非阻塞任务,take是阻塞状态,也就是实现执行take时候,无法向下继续执行,fork是非阻塞的,同样可以使用cancel取消一个fork 任务。

    function* authorize(user, password) {
      try {
        const token = yield call(Api.authorize, user, password)
        yield put({type: 'LOGIN_SUCCESS', token})
      } catch(error) {
        yield put({type: 'LOGIN_ERROR', error})
      }
    }
    
    function* loginFlow() {
      while(true) {
        const {user, password} = yield take('LOGIN_REQUEST')
        yield fork(authorize, user, password)
        yield take(['LOGOUT', 'LOGIN_ERROR'])
        yield call(Api.clearItem('token'))
      }
    }

    上面例子中,当执行

    yield fork(authorize, user, password)

    的同时,也执行了下面代码,进行logout的监听操作。

    yield take(['LOGOUT', 'LOGIN_ERROR'])

    redux-saga使用案例

    引入saga:

    import { call, put, take, select } from 'redux-saga/effects';

    创建任务:

    /**
     * Created by Richard on 1/11/17.
     */
    import { call, put, take, select } from 'redux-saga/effects';
    import { get, getWordUrl } from '../../utils/api';
    import {successFetchData } from './WordAction';
    
    export default function* wordFlow() {
            try {
                    const data = yield call(get, getWordUrl());
                    yield put(successFetchData(data));
            } catch (e){
    
            }
    }

    创建saga:

    import { fork } from 'redux-saga/effects';
    import wordFlow from './containers/word-view/WordSaga';
    
    export default function* rootSaga() {
      yield [
        fork(wordFlow)
      ];
    }

    与redux中间件进行整合:

    /**
     * Created by Richard on 12/29/16.
     */
    import { applyMiddleware, createStore, compose } from 'redux';
    
    import createSagaMiddleware from 'redux-saga';
    
    import reducers from '../reducers';
    
    import sagas from '../sagas';
    
    //创建saga middleware
    const sagaMiddleware = createSagaMiddleware();
    
    const middlewares = compose(applyMiddleware(sagaMiddleware)autoRehydrate());
    
    export default function configureStore() {
            const store = createStore(reducers, undefined, middlewares);
            //运行所有已经注册的saga
            sagaMiddleware.run(sagas);
            return store;
    }

    下面就可以正常监听状态了。

    redux-saga的优势

    传统意义讲,我们很多业务逻辑要在action中处理,所以会导致action的处理比较混乱,难以维护,而且代码量比较大,如果我们应用redux-saga会很大程度上简化代码, redux-saga 本身也有良好的扩展性, 非常方便的处理各种复杂的异步问题。

    更多信息,请查看官方文档:

    https://redux-saga.github.io/redux-saga/

    Redux-saga使用心得总结(包含样例代码),本文的样例代码地址:样例代码地址 ,欢迎star


    最近将项目中redux的中间件,从redux-thunk替换成了redux-saga,做个笔记总结一下redux-saga的使用心得,阅读本文需要了解什么是redux,redux中间件的用处是什么?如果弄懂上述两个概念,就可以继续阅读本文。

    • redux-thunk处理副作用的缺点
    • redux-saga写一个hellosaga
    • redux-saga的使用技术细节
    • redux-saga实现一个登陆和列表样例

    1.redux-thunk处理副作用的缺点

    (1)redux的副作用处理

    redux中的数据流大致是:

    UI—————>action(plain)—————>reducer——————>state——————>UI

    default

    redux是遵循函数式编程的规则,上述的数据流中,action是一个原始js对象(plain object)且reducer是一个纯函数,对于同步且没有副作用的操作,上述的数据流起到可以管理数据,从而控制视图层更新的目的。

    但是如果存在副作用,比如ajax异步请求等等,那么应该怎么做?

    如果存在副作用函数,那么我们需要首先处理副作用函数,然后生成原始的js对象。如何处理副作用操作,在redux中选择在发出action,到reducer处理函数之间使用中间件处理副作用。

    redux增加中间件处理副作用后的数据流大致如下:

    UI——>action(side function)—>middleware—>action(plain)—>reducer—>state—>UI

    default

    在有副作用的action和原始的action之间增加中间件处理,从图中我们也可以看出,中间件的作用就是:

    转换异步操作,生成原始的action,这样,reducer函数就能处理相应的action,从而改变state,更新UI。

    (2)redux-thunk

    在redux中,thunk是redux作者给出的中间件,实现极为简单,10多行代码:

    function createThunkMiddleware(extraArgument) {
      return ({ dispatch, getState }) => next => action => {
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }
    
        return next(action);
      };
    }
    
    const thunk = createThunkMiddleware();
    thunk.withExtraArgument = createThunkMiddleware;
    
    export default thunk;
    

    这几行代码做的事情也很简单,判别action的类型,如果action是函数,就调用这个函数,调用的步骤为:

    action(dispatch, getState, extraArgument);
    

    发现实参为dispatch和getState,因此我们在定义action为thunk函数是,一般形参为dispatch和getState。

    (3)redux-thunk的缺点

    hunk的缺点也是很明显的,thunk仅仅做了执行这个函数,并不在乎函数主体内是什么,也就是说thunk使
    得redux可以接受函数作为action,但是函数的内部可以多种多样。比如下面是一个获取商品列表的异步操作所对应的action:

    export default ()=>(dispatch)=>{
        fetch('/api/goodList',{ //fecth返回的是一个promise
          method: 'get',
          dataType: 'json',
        }).then(function(json){
          var json=JSON.parse(json);
          if(json.msg==200){
            dispatch({type:'init',data:json.data});
          }
        },function(error){
          console.log(error);
        });
    };
    

    从这个具有副作用的action中,我们可以看出,函数内部极为复杂。如果需要为每一个异步操作都如此定义一个action,显然action不易维护。

    action不易维护的原因:

    • action的形式不统一
    • 就是异步操作太为分散,分散在了各个action中

    2.redux-saga写一个hellosaga

    跟redux-thunk,redux-saga是控制执行的generator,在redux-saga中action是原始的js对象,把所有的异步副作用操作放在了saga函数里面。这样既统一了action的形式,又使得异步操作集中可以被集中处理。

    redux-saga是通过genetator实现的,如果不支持generator需要通过插件babel-polyfill转义。我们接着来实现一个输出hellosaga的例子。

    (1)创建一个helloSaga.js文件

    export function * helloSaga() {
      console.log('Hello Sagas!');
    }
    

    (2)在redux中使用redux-saga中间件

    在main.js中:

    import { createStore, applyMiddleware } from 'redux'
    import createSagaMiddleware from 'redux-saga'
    import { helloSaga } from './sagas'
    const sagaMiddleware=createSagaMiddleware();
    const store = createStore(
     reducer,
     applyMiddleware(sagaMiddleware)
    );
    sagaMiddleware.run(helloSaga);
    //会输出Hello, Sagas!
    

    和调用redux的其他中间件一样,如果想使用redux-saga中间件,那么只要在applyMiddleware中调用一个createSagaMiddleware的实例。唯一不同的是需要调用run方法使得generator可以开始执行。

    3.redux-saga的使用技术细节

    redux-saga除了上述的action统一、可以集中处理异步操作等优点外,redux-saga中使用声明式的Effect以及提供了更加细腻的控制流。

    (1)声明式的Effect

    redux-saga中最大的特点就是提供了声明式的Effect,声明式的Effect使得redux-saga监听原始js对象形式的action,并且可以方便单元测试,我们一一来看。

    • 首先,在redux-saga中提供了一系列的api,比如take、put、all、select等API ,在redux-saga中将这一系列的api都定义为Effect。这些Effect执行后,当函数resolve时返回一个描述对象,然后redux-saga中间件根据这个描述对象恢复执行generator中的函数。

    首先来看redux-thunk的大体过程:

    action1(side function)—>redux-thunk监听—>执行相应的有副作用的方法—>action2(plain object)

    2

    转化到action2是一个原始js对象形式的action,然后执行reducer函数就会更新store中的state。

    而redux-saga的大体过程如下:

    action1(plain object)——>redux-saga监听—>执行相应的Effect方法——>返回描述对象—>恢复执行异步和副作用函数—>action2(plain object)

    default

    对比redux-thunk我们发现,redux-saga中监听到了原始js对象action,并不会马上执行副作用操作,会先通过Effect方法将其转化成一个描述对象,然后再将描述对象,作为标识,再恢复执行副作用函数。

    通过使用Effect类函数,可以方便单元测试,我们不需要测试副作用函数的返回结果。只需要比较执行Effect方法后返回的描述对象,与我们所期望的描述对象是否相同即可。

    举例来说,call方法是一个Effect类方法:

    import { call } from 'redux-saga/effects'
    
    function* fetchProducts() {
      const products = yield call(Api.fetch, '/products')
      // ...
    }
    

    上述代码中,比如我们需要测试Api.fetch返回的结果是否符合预期,通过调用call方法,返回一个描述对象。这个描述对象包含了所需要调用的方法和执行方法时的实际参数,我们认为只要描述对象相同,也就是说只要调用的方法和执行该方法时的实际参数相同,就认为最后执行的结果肯定是满足预期的,这样可以方便的进行单元测试,不需要模拟Api.fetch函数的具体返回结果。

    import { call } from 'redux-saga/effects'
    import Api from '...'
    
    const iterator = fetchProducts()
    
    // expects a call instruction
    assert.deepEqual(
      iterator.next().value,
      call(Api.fetch, '/products'),
      "fetchProducts should yield an Effect call(Api.fetch, './products')"
    )
    

    (2)Effect提供的具体方法

    下面来介绍几个Effect中常用的几个方法,从低阶的API,比如take,call(apply),fork,put,select等,以及高阶API,比如takeEvery和takeLatest等,从而加深对redux-saga用法的认识(这节可能比较生涩,在第三章中会结合具体的实例来分析,本小节先对各种Effect有一个初步的了解)。

    引入:

    import {take,call,put,select,fork,takeEvery,takeLatest} from 'redux-saga/effects'
    
    • take

    take这个方法,是用来监听action,返回的是监听到的action对象。比如:

    const loginAction = {
       type:'login'
    }
    

    在UI Component中dispatch一个action:

    dispatch(loginAction)
    

    在saga中使用:

    const action = yield take('login');
    

    可以监听到UI传递到中间件的Action,上述take方法的返回,就是dipath的原始对象。一旦监听到login动作,返回的action为:

    {
      type:'login'
    }
    
    • call(apply)

    call和apply方法与js中的call和apply相似,我们以call方法为例:

    call(fn, ...args)
    

    call方法调用fn,参数为args,返回一个描述对象。不过这里call方法传入的函数fn可以是普通函数,也可以是generator。call方法应用很广泛,在redux-saga中使用异步请求等常用call方法来实现。

    yield call(fetch,'/userInfo',username)
    
    • put

    在前面提到,redux-saga做为中间件,工作流是这样的:

    UI——>action1————>redux-saga中间件————>action2————>reducer..

    从工作流中,我们发现redux-saga执行完副作用函数后,必须发出action,然后这个action被reducer监听,从而达到更新state的目的。相应的这里的put对应与redux中的dispatch,工作流程图如下:

    default

    从图中可以看出redux-saga执行副作用方法转化action时,put这个Effect方法跟redux原始的dispatch相似,都是可以发出action,且发出的action都会被reducer监听到。put的使用方法:

     yield put({type:'login'})
    
    • select

    put方法与redux中的dispatch相对应,同样的如果我们想在中间件中获取state,那么需要使用select。select方法对应的是redux中的getState,用户获取store中的state,使用方法:

    const state= yield select()
    
    • fork

    fork方法在第三章的实例中会详细的介绍,这里先提一笔,fork方法相当于web work,fork方法不会阻塞主线程,在非阻塞调用中十分有用。

    • takeEvery和takeLatest

    takeEvery和takeLatest用于监听相应的动作并执行相应的方法,是构建在take和fork上面的高阶api,比如要监听login动作,好用takeEvery方法可以:

    takeEvery('login',loginFunc)
    

    takeEvery监听到login的动作,就会执行loginFunc方法,除此之外,takeEvery可以同时监听到多个相同的action。

    takeLatest方法跟takeEvery是相同方式调用:

    takeLatest('login',loginFunc)
    

    与takeLatest不同的是,takeLatest是会监听执行最近的那个被触发的action。

    4.redux-saga实现一个登陆和列表样例

    接着我们来实现一个redux-saga样例,存在一个登陆页,登陆成功后,显示列表页,并且,在列表页,可

    以点击登出,返回到登陆页。例子的最终展示效果如下:

    login

    样例的功能流程图为:

    default

    接着我们按照上述的流程来一步步的实现所对应的功能。

    (1)LoginPanel(登陆页)

    登陆页的功能包括

    • 输入时时保存用户名
    • 输入时时保存密码
    • 点击sign in 请求判断是否登陆成功

    I)输入时时保存用户名和密码

    用户名输入框和密码框onchange时触发的函数为:

     changeUsername:(e)=>{
        dispatch({type:'CHANGE_USERNAME',value:e.target.value});
     },
    changePassword:(e)=>{
      dispatch({type:'CHANGE_PASSWORD',value:e.target.value});
    }
    

    在函数中最后会dispatch两个action:CHANGE_USERNAME和CHANGE_PASSWORD

    在saga.js文件中监听这两个方法并执行副作用函数,最后put发出转化后的action,给reducer函数调用:

    function * watchUsername(){
      while(true){
        const action= yield take('CHANGE_USERNAME');
        yield put({type:'change_username',
        value:action.value});
      }
    }
    function * watchPassword(){
      while(true){
        const action=yield take('CHANGE_PASSWORD');
        yield put({type:'change_password',
        value:action.value});
      }
    }
    

    最后在reducer中接收到redux-saga的put方法传递过来的action:change_username和change_password,然后更新state。

    II)监听登陆事件判断登陆是否成功

    在UI中发出的登陆事件为:

    toLoginIn:(username,password)=>{
      dispatch({type:'TO_LOGIN_IN',username,password});
    }
    

    登陆事件的action为:TO_LOGIN_IN.对于登入事件的处理函数为:

     while(true){
        //监听登入事件
        const action1=yield take('TO_LOGIN_IN');
        const res=yield call(fetchSmart,'/login',{
          method:'POST',
          body:JSON.stringify({
            username:action1.username,
            password:action1.password
        })
        if(res){
          put({type:'to_login_in'});
        }
    });
    

    在上述的处理函数中,首先监听原始动作提取出传递来的用户名和密码,然后请求是否登陆成功,如果登陆成功有返回值,则执行put的action:to_login_in.

    (2) LoginSuccess(登陆成功列表展示页)

    登陆成功后的页面功能包括:

    • 获取列表信息,展示列表信息
    • 登出功能,点击可以返回登陆页面

    I)获取列表信息

    import {delay} from 'redux-saga';
    
    function * getList(){
      try {
       yield delay(3000);
       const res = yield call(fetchSmart,'/list',{
         method:'POST',
         body:JSON.stringify({})
       });
       yield put({type:'update_list',list:res.data.activityList});
     } catch(error) {
       yield put({type:'update_list_error', error});
     }
    }
    

    为了演示请求过程,我们在本地mock,通过redux-saga的工具函数delay,delay的功能相当于延迟xx秒,因为真实的请求存在延迟,因此可以用delay在本地模拟真实场景下的请求延迟。

    II)登出功能

    const action2=yield take('TO_LOGIN_OUT');
    yield put({type:'to_login_out'});
    

    与登入相似,登出的功能从UI处接受action:TO_LOGIN_OUT,然后转发action:to_login_out

    (3) 完整的实现登入登出和列表展示的代码

    function * getList(){
      try {
       yield delay(3000);
       const res = yield call(fetchSmart,'/list',{
         method:'POST',
         body:JSON.stringify({})
       });
       yield put({type:'update_list',list:res.data.activityList});
     } catch(error) {
       yield put({type:'update_list_error', error});
     }
    }
    
    function * watchIsLogin(){
      while(true){
        //监听登入事件
        const action1=yield take('TO_LOGIN_IN');
        
        const res=yield call(fetchSmart,'/login',{
          method:'POST',
          body:JSON.stringify({
            username:action1.username,
            password:action1.password
          })
        });
        
        //根据返回的状态码判断登陆是否成功
        if(res.status===10000){
          yield put({type:'to_login_in'});
          //登陆成功后获取首页的活动列表
          yield call(getList);
        }
        
        //监听登出事件
        const action2=yield take('TO_LOGIN_OUT');
        yield put({type:'to_login_out'});
      }
    }
    

    通过请求状态码判断登入是否成功,在登陆成功后,可以通过:

    yield call(getList)
    

    的方式调用获取活动列表的函数getList。这样咋一看没有什么问题,但是注意call方法调用是会阻塞主线程的,具体来说:

    • 在call方法调用结束之前,call方法之后的语句是无法执行的

    • 如果call(getList)存在延迟,call(getList)之后的语句 const action2=yieldtake('TO_LOGIN_OUT')在call方法返回结果之前无法执行

    • 在延迟期间的登出操作会被忽略。

    用框图可以更清楚的分析:

    default

    call方法调用阻塞主线程的具体效果如下动图所示:

    login_1

    白屏时为请求列表的等待时间,在此时,我们点击登出按钮,无法响应登出功能,直到请求列表成功,展示列表信息后,点击登出按钮才有相应的登出功能。也就是说call方法阻塞了主线程。

    (4) 无阻塞调用

    我们在第二章中,介绍了fork方法可以类似与web work,fork方法不会阻塞主线程。应用于上述例子,我们可以将:

    yield call(getList)
    

    修改为:

    yield fork(getList)
    

    这样展示的结果为:

    login_2

    通过fork方法不会阻塞主线程,在白屏时点击登出,可以立刻响应登出功能,从而返回登陆页面。

    5.总结

    通过上述章节,我们可以概括出redux-saga做为redux中间件的全部优点:

    • 统一action的形式,在redux-saga中,从UI中dispatch的action为原始对象

    • 集中处理异步等存在副作用的逻辑

    • 通过转化effects函数,可以方便进行单元测试

    • 完善和严谨的流程控制,可以较为清晰的控制复杂的逻辑。

    本文主要来自:https://github.com/forthealllight/blog/issues/14

    正因为当初对未来做了太多的憧憬,所以对现在的自己尤其失望。生命中曾经有过的所有灿烂,终究都需要用寂寞来偿还。
  • 相关阅读:
    linux挂载windows共享文件夹
    centos-6.7 内核升级(转)
    关闭系统邮件提醒:you hava a new mail(转)
    2016年国内开源镜像站点汇总(转)
    RedHat/Fedora/Centos 下bash 自动补全命令
    shell死循环脚本示例
    查看http的并发请求数与其TCP连接状态
    (十二)微信小程序实现登陆页面+登陆逻辑
    vue实现登陆单页面
    vue前端项目初始化的步骤
  • 原文地址:https://www.cnblogs.com/candlia/p/11920064.html
Copyright © 2011-2022 走看看