不折不扣教会你使用Redux-saga(包含样例代码)

Redux-saga使用心得总结(包含样例代码),git

本文的原文地址:原文地址github

本文的样例代码地址:样例代码地址 ,欢迎starweb


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

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

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

(1)redux的反作用处理

redux中的数据流大体是:编程

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

default

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

可是若是存在反作用,好比ajax异步请求等等,那么应该怎么作?api

若是存在反作用函数,那么咱们须要首先处理反作用函数,而后生成原始的js对象。如何处理反作用操做,在redux中选择在发出action,到reducer处理函数之间使用中间件处理反作用。promise

redux增长中间件处理反作用后的数据流大体以下:babel

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统1、能够集中处理异步操做等优势外,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函数,能够方便进行单元测试

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

相关文章
相关标签/搜索