redux-thunk与redux-saga中间件详解

栏目: 服务器 · 发布时间: 5年前

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

redux中的数据流大致是

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

redux-thunk与redux-saga中间件详解

  • redux 是遵循函数式编程的规则,上述的数据流中, action 是一个原始js对象( plain object )且 reducer 是一个纯函数,对于同步且没有副作用的操作,上述的数据流起到可以管理数据,从而控制视图层更新的目的
  • 如果存在副作用函数,那么我们需要首先处理副作用函数,然后生成原始的js对象。如何处理副作用操作,在 redux 中选择在发出 action ,到 reducer 处理函数之间使用中间件处理副作用

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

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

redux-thunk与redux-saga中间件详解

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

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

1.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);

发现实参为 dispatchgetState ,因此我们在定义 actionthunk 函数是,一般形参为 dispatchgetState

1.3 redux-thunk的缺点

thunk 的缺点也是很明显的, 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

二、redux-saga 简介

redux-saga 是一个 redux 中间件,它具有如下特性

  • 集中处理 redux 副作用问题。
  • 被实现为 generator
  • redux-thunk 中间件。
  • watch / worker (监听->执行) 的工作形式

redux-saga的优点

  • 集中处理了所有的异步操作,异步接口部分一目了然
  • action 是普通对象,这跟 redux 同步的 action 一模一样
  • 通过 Effect ,方便异步接口的测试
  • 通过 workerwatcher 可以实现非阻塞异步调用,并且同时可以实现非阻塞调用下的事件监听
  • 异步操作的流程是可以控制的,可以随时取消相应的异步操作

基本用法

  • 使用 createSagaMiddleware 方法创建 sagaMiddleware ,然后在创建的 reduxstore 时,使用 applyMiddleware 函数将创建的 saga Middleware 实例绑定到 store 上,最后可以调用 saga Middlewarerun 函数来执行某个或者某些 Middleware
  • sagaMiddleware 中,可以使用 takeEvery 或者 takeLatestAPI 来监听某个 action ,当某个 action 触发后, saga 可以使用 call 发起异步操作,操作完成后使用 put 函数触发 action ,同步更新 state ,从而完成整个 State 的更新。

三、redux-saga使用案例

  • redux-saga 是控制执行的 generator ,在 redux-sagaaction 是原始的 js 对象,把所有的异步副作用操作放在了 saga 函数里面。这样既统一了 action 的形式,又使得异步操作集中可以被集中处理
  • redux-saga 是通过 genetator 实现的,如果不支持 generator 需要通过插件 babel-polyfill 转义。我们接着来实现一个输出 hellosaga 的例子

创建一个helloSaga.js文件

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

在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 可以开始执行

四、redux-saga使用细节

4.1 声明式的Effect

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

redux-thunk的大体过程

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

redux-thunk与redux-saga中间件详解

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

redux-saga的大体过程

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

redux-thunk与redux-saga中间件详解

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

4.2 Effect提供的具体方法

下面来介绍几个 Effect 中常用的几个方法,从低阶的API,比如 takecall(apply)forkputselect 等,以及高阶 API ,比如 takeEverytakeLatest

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

4.2.1 take

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

const loginAction = {
   type:'login'
}

UI Componentdispatch 一个 action

dispatch(loginAction)

在saga中使用:

const action = yield take('login');

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

{
  type:'login'
}

4.2.2 call(apply)

callapply 方法与 js 中的 callapply 相似,我们以 call 方法为例

call(fn, ...args)

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

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

4.2.3 put

redux-saga做为中间件,工作流是这样的

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

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

redux-thunk与redux-saga中间件详解

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

yield put({type:'login'})

4.2.4 select

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

const id = yield select(state => state.id);

4.2.5 fork

fork 方法相当于 web workfork 方法不会阻塞主线程,在非阻塞调用中十分有用

4.2.6 takeEvery和takeLatest

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

takeEvery('login',loginFunc)
  • takeEvery 监听到 login 的动作,就会执行 loginFunc 方法,除此之外, takeEvery 可以同时监听到多个相同的 action
  • takeLatest 方法跟 takeEvery 是相同方式调用
takeLatest('login',loginFunc)

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

五、结合案例分析

接着我们来实现一个 redux-saga 样例,存在一个登陆页,登陆成功后,显示列表页,并且,在列表页,可以点击登出,返回到登陆页。例子的最终展示效果如下

redux-thunk与redux-saga中间件详解

样例的功能流程图为

redux-thunk与redux-saga中间件详解

5.1 LoginPanel(登陆页)

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

  • 用户名输入框和密码框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-sagaput 方法传递过来的 action:change_usernamechange_password ,然后更新 state

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

在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'});
    }
});

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

5.2 LoginSuccess

(登陆成功列表展示页)

  • 登陆成功后的页面功能包括:
    • 获取列表信息,展示列表信息
    • 登出功能,点击可以返回登陆页面

获取列表信息

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工具 函数 delaydelay 的功能相当于延迟xx秒,因为真实的请求存在延迟,因此可以用delay在本地模拟真实场景下的请求延迟

登出功能

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

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

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

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)

注意call方法调用是会阻塞主线程的,具体来说

  • 在call方法调用结束之前,call方法之后的语句是无法执行的
  • 如果 call(getList) 存在延迟, call(getList) 之后的语句 const action2=yieldtake('TO_LOGIN_OUT')call 方法返回结果之前无法执行
  • 在延迟期间的登出操作会被忽略

redux-thunk与redux-saga中间件详解

无阻塞调用

yield call(getList)

修改为

yield fork(getList)

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

六、总结

redux-saga 做为 redux 中间件的全部优点

  • 统一 action 的形式,在 redux-saga 中,从 UIdispatchaction 为原始对象
  • 集中处理异步等存在副作用的逻辑
  • 通过转化 effects 函数,可以方便进行单元测试
  • 完善和严谨的流程控制,可以较为清晰的控制复杂的逻辑

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Haskell

Haskell

Simon Thompson / Addison-Wesley / 1999-3-16 / GBP 40.99

The second edition of Haskell: The Craft of Functional Programming is essential reading for beginners to functional programming and newcomers to the Haskell programming language. The emphasis is on th......一起来看看 《Haskell》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具