Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

彻彻底底教会你使用Redux-saga(包含样例代码) #14

Open
forthealllight opened this issue Jun 27, 2018 · 29 comments
Open

彻彻底底教会你使用Redux-saga(包含样例代码) #14

forthealllight opened this issue Jun 27, 2018 · 29 comments

Comments

@forthealllight
Copy link
Owner

forthealllight commented Jun 27, 2018

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函数,可以方便进行单元测试

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

@forthealllight forthealllight changed the title Redux-saga使用心得和总结 Redux-saga使用心得总结(包含样例代码) Jul 2, 2018
@forthealllight forthealllight changed the title Redux-saga使用心得总结(包含样例代码) 彻彻底底教会你使用Redux-saga(包含样例代码) Jul 4, 2018
@FantasticPerson
Copy link

写的很不错

@forthealllight
Copy link
Owner Author

谢谢哈

@codering
Copy link

👍

@amandakelake
Copy link

写的很详细,女生就是细腻一点
冒昧问一下,楼主最后去了哪家公司呢?

@forthealllight
Copy link
Owner Author

谢谢表扬哈,去了一家小公司哈

@dylan-shao
Copy link

写的很清晰易懂👍

有一个小问题, 为什么要用saga 来 watch用户输入账户和密码呢?这个过程没有异步操作, 可以直接被reducer捕捉到。

@forthealllight
Copy link
Owner Author

是的,确实不需要用saga

@AlanWenhao
Copy link

你好,使用saga我有一个问题:

关于: UI 中获取异步请求结果后,执行后续 UI 操作

我在UI中调用 action 触发异步请求,例子中成功后我可以修改 store。
但是我想在 UI 中异步请求结束后能够 alert('请求结束')
这里怎么写比较好?

@forthealllight
Copy link
Owner Author

你好,使用saga我有一个问题:

关于: UI 中获取异步请求结果后,执行后续 UI 操作

我在UI中调用 action 触发异步请求,例子中成功后我可以修改 store。
但是我想在 UI 中异步请求结束后能够 alert('请求结束')
这里怎么写比较好?

首先先接受一个action,通过redux-saga中的take,然后通过redux-saga中的call 方法调用一个请求函数 yield call( ...fetchURL,params) , yield会等待call方法返回结果后再往下执行,调用yield call请求后,你就可以alert('请求结束') , 如果不想用window.alert(), 你可以在store中保存alert是否显示的变量,在yield call后,改变这个变量的值,使得自定义的alert弹窗显示

@AlanWenhao
Copy link

@forthealllight
嗯好的,谢谢你的回答
其实我想实现的是类似 dva 一样的操作(像下面的操作),这个我再去看下吧
https://github.com/zuiidea/antd-admin/blob/master/src/pages/user/index.js#L38-L46

@nbhaohao
Copy link

nbhaohao commented Dec 28, 2018

想请教一下,为什么实例代码中的 generator 函数里面要写 while(true) 来包裹逻辑?
例如:

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

@forthealllight
Copy link
Owner Author

想请教一下,为什么实例代码中的 generator 函数里面要写 while(true) 来包裹逻辑?
例如:

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

您好,看到你的问题。第一时间来回答你,因为type = ‘CHANGE_USERNAME’ 动作不可能只发生一次。
我这里的业务要求,只要发生了该动作就会执行相应的逻辑。whilte(true) 的意思是一直监听,被监听的动作不止一次的发生。

当然,你如果只需要监听一次动作, 那么就不需要while循环。

@nbhaohao
Copy link

有些明白了,非常感谢帮助!

@forthealllight
Copy link
Owner Author

有些明白了,非常感谢帮助!

不客气~

@wangziling
Copy link

2.redux-saga写一个hellosaga
跟redux-thunk,redux-saga是控制执行的generator

这儿应该是 跟 redux-thunk 不同的是, 吧。

顺便,感谢 blog 的帮助-.-

@lilililee
Copy link

有几点疑问想请教一下:

  1. 把请求全部放在saga里面,如果UI-components在请求成功后有其他操作如何处理?比如要修改页面某个图标颜色。
  2. 文中所诉模式是不是把数据全部存在了store,而弃用了state,在大型单页项目中会不会有性能问题。
  3. while(true)写法是不是等同于takeEvery?

@forthealllight
Copy link
Owner Author

2.redux-saga写一个hellosaga
跟redux-thunk,redux-saga是控制执行的generator

这儿应该是 跟 redux-thunk 不同的是, 吧。

顺便,感谢 blog 的帮助-.-

感谢指正,已经修改

@forthealllight
Copy link
Owner Author

有几点疑问想请教一下:

  1. 把请求全部放在saga里面,如果UI-components在请求成功后有其他操作如何处理?比如要修改页面某个图标颜色。
  2. 文中所诉模式是不是把数据全部存在了store,而弃用了state,在大型单页项目中会不会有性能问题。
  3. while(true)写法是不是等同于takeEvery?

1、UI在请求后如何处理,一般是这样子,在saga中请求,请求完成后更新store中的state,而UI-components从store中取新的state,以props的形式传递,从而更新整个视图。这里跟你用redux-thunk一样的,仅仅是一个异步的中间件。本质还是redux做状态管理。

2、我知道你说所的可能是无效render。在设计redux树的时候,可以选择完全弃用局部的state,将所有状态保存在store中,这种情况不会有性能问题,可以通过SCU来控制渲染实现你所说的优化。当然,有些也不需要完全弃用局部state,你这个问题本质是一个如何设计redux状态树的问题。

总结: 1、2 两个问题其实本质跟redux-saga无关,是redux的问题

3、可以理解成

whille(true){
const action = yield take(someaction)
}

等价于

const action yield takeEvery(someaction);

本质上takeEvery是根据take来实现的一个上层的API。

最后,希望能对你有所帮助~

@lilililee
Copy link

@forthealllight 感谢解答,很有帮助~

@forthealllight
Copy link
Owner Author

forthealllight commented Jan 21, 2019

@forthealllight 感谢解答,很有帮助~

还有一个补充,takeEvery虽然是一个顶层API,但是他是action发起时,被push到任务执行器然后执行的。而用底层API take的好处,是去pull新任务,然后执行。

两者的却别就是使用take可以实现更复杂的控制逻辑,比如使用takeEvery无法结束监听,而底层的take就很方便。

简单来说,可以用一句话概括:底层的API,实现细节更加可控哦~

不用谢

@tkgkn
Copy link

tkgkn commented Jan 25, 2019

你好,有个问题请教下。看了下你另一个仓库saga-example中的例子,因为是只有登录和获取列表的异步,所以saga的相关代码是放在src/saga/index.js中。
如果是较复杂的应用,saga如何组织呢?是否能够和combineReducers一样将不同页面的reducers合并到一起,根据不同页面就近维护自己的saga,然后最终在store/index.js中import各个页面自己的saga,统一到sagaMiddleware.run(rootSaga)中。

@forthealllight
Copy link
Owner Author

你好,有个问题请教下。看了下你另一个仓库saga-example中的例子,因为是只有登录和获取列表的异步,所以saga的相关代码是放在src/saga/index.js中。
如果是较复杂的应用,saga如何组织呢?是否能够和combineReducers一样将不同页面的reducers合并到一起,根据不同页面就近维护自己的saga,然后最终在store/index.js中import各个页面自己的saga,统一到sagaMiddleware.run(rootSaga)中。

当然可以,这个就是我所说的redux-dark模式。你可以参考我的这篇文章:#41

@tkgkn
Copy link

tkgkn commented Jan 25, 2019

@forthealllight 谢谢。我尝试下你所说的redux-dark

@liushengjianying
Copy link

小姐姐你好,请问下实际业务中,saga.js这个文件怎么拆分呢,不可能不同业务几十个函数全放一个文件

@forthealllight
Copy link
Owner Author

小姐姐你好,请问下实际业务中,saga.js这个文件怎么拆分呢,不可能不同业务几十个函数全放一个文件

我是按页面拆分的,一个页面对应一个saga文件,然后在根目录require到一起的哦,具体可以看我的这篇文章#41

@liushengjianying
Copy link

小姐姐你好,请问下实际业务中,saga.js这个文件怎么拆分呢,不可能不同业务几十个函数全放一个文件

我是按页面拆分的,一个页面对应一个saga文件,然后在根目录require到一起的哦,具体可以看我的这篇文章#41

那这样的话,reducer的状态会不会被拆的太散了,比如 2个页面可能复用一个reducer。

@forthealllight
Copy link
Owner Author

小姐姐你好,请问下实际业务中,saga.js这个文件怎么拆分呢,不可能不同业务几十个函数全放一个文件

我是按页面拆分的,一个页面对应一个saga文件,然后在根目录require到一起的哦,具体可以看我的这篇文章#41

那这样的话,reducer的状态会不会被拆的太散了,比如 2个页面可能复用一个reducer。

保持一个页面一个reducer的拆分

@ywAscend
Copy link

感谢小姐姐,学习了

@gaofeng222
Copy link

目前硬要我挑个毛病的话,redux-thunk的缺点下面的单词少了个t

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests