Redux-Saga入门

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

1.1 redux的副作用处理

redux中的数据流大致是:

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

redux数据流

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

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

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

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

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

有副作用的数据流

在有副作用的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);

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

1.3 redux-thunk的缺点

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

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

从这个具有副作用的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转义。

2.1 创建一个helloSaga.js文件

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

2.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可以开始执行。

2.3 redux-saga的使用技术细节

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

2.3.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)

redux-thunk

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

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

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

对比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.3.2 Effect提供的具体方法

下面来介绍几个Effect中常用的几个方法,从低阶的API,比如take,call(apply),fork,put,select等,以及高阶API,比如takeEvery和takeLatest等,从而加深对redux-saga用法的认识
引入:

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方法的返回,就是dispatch的原始对象。一旦监听到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,工作流程图如下:

从图中可以看出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方法相当于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。

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

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

样例的功能流程图为:

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

2.3.4.1 LoginPanel(登陆页)

登陆页的功能包括

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

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

用户名输入框和密码框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文件中监听这两个action并执行副作用函数,最后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。

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

在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.3.4.2 LoginSuccess(登陆成功列表展示页)

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

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

1. 获取列表信息

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在本地模拟真实场景下的请求延迟。

2. 登出功能

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方法返回结果之前无法执行

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

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

image.png

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

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

2.3.4.3 无阻塞调用

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

yield call(getList)

修改为:

yield fork(getList)

这样展示的结果为:

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

3. 总结

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

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

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容