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

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

内容简介: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 函数,可以方便进行单元测试
  • 完善和严谨的流程控制,可以较为清晰的控制复杂的逻辑

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

查看所有标签

猜你喜欢:

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

Python 3网络爬虫开发实战

Python 3网络爬虫开发实战

崔庆才 / 人民邮电出版社 / 2018-4 / 99

本书介绍了如何利用Python 3开发网络爬虫,书中首先介绍了环境配置和基础知识,然后讨论了urllib、requests、正则表达式、Beautiful Soup、XPath、pyquery、数据存储、Ajax数据爬取等内容,接着通过多个案例介绍了不同场景下如何实现数据爬取,后介绍了pyspider框架、Scrapy框架和分布式爬虫。 本书适合Python程序员阅读。一起来看看 《Python 3网络爬虫开发实战》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具