007 Redux-saga

8/4/2021 Javascript

# 简介

Redux-sagaRedux-Thunk 最大的不同在于,redux-saga又回到了纯对象模式,并且通过监听的方式来执行副作用函数。而 redux-thunk 则直接执行副作用函数,异步请求结束后直接发起action,并且与 Redux-Thunk 相比,Redux-Saga 的好处是你可以更好得控制业务流程,增强了代码的可读性,在 saga 之前,你可能会在 action creator 里处理业务逻辑,虽然能跑通,但是难以测试。比如:

// action creator with thunking
function createRequest () {
  return (dispatch, getState) => {
    dispatch({ type: 'REQUEST_STUFF' });
    someApiCall(function(response) {
      // some processing
      dispatch({ type: 'RECEIVE_STUFF' });
    });
  };
}
// ui
function onHandlePress () {
  this.props.dispatch({ type: 'SHOW_WAITING_MODAL' });
  this.props.dispatch(createRequest());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这样通过 redux state 和 reducer 把所有的事情串联到起来。

但问题是:

Code is everywhere.

# saga

通过 saga,你只需要触发一个 action 。

function onHandlePress () {
  // createRequest 触发 action `BEGIN_REQUEST`
  this.props.dispatch(createRequest());
}
1
2
3
4

然后所有后续的操作都通过 saga 来管理。

function *hello() {
  // 等待 action `BEGIN_REQUEST`
  yield take('BEGIN_REQUEST');
  // dispatch action `SHOW_WAITING_MODAL`
  yield put({ type: 'SHOW_WAITING_MODAL' });
  // 发布异步请求
  const response = yield call(myApiFunctionThatWrapsFetch);
  // dispatch action `PRELOAD_IMAGES`, 附上 response 信息
  yield put({ type: 'PRELOAD_IMAGES', response.images });
  // dispatch action `HIDE_WAITING_MODAL`
  yield put({ type: 'HIDE_WAITING_MODAL' });
}
1
2
3
4
5
6
7
8
9
10
11
12

可以看出,调整之后的代码有几个优点:

  • 所有业务代码都存于 saga 中,不再散落在各处
  • 全同步执行,就算逻辑再复杂,看起来也不会乱

总结以下就是 Thunk 比较适合一些小项目,操作简单,逻辑包含在函数内部,不需要学习Generator函数及操作流程,学习成本较低,应该根据项目的复杂度决定使用何种中间件。

# 使用

使用 saga 最重要的一点是监听,网上很多文章都把这一点省略掉了,根据下图实现一个简单的加减操作 foo

# ui

import React from "react";
import { connect } from "react-redux";
function Hello(props) {
  return (
    <div>
      <span>{props.number}</span>
      <button onClick={props.handleAdd}>+</button>;
    </div>
  );
}
const mapStateToProps = (state) => {
  return {
    number: state.reducer2.number
  };
};
const mapDispatchToProps = {
  handleAdd: () => ({ type: "ADD_NUMBER" }) // 这个 action 将被saga拦截
};
export default connect(mapStateToProps, mapDispatchToProps)(Hello);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# saga

function* addFun() {
  yield put({ type: "ADD_NUMBER_FOR_REDUCER" }); // 发起派生action用于更新state
}
function* watchAdd() {
  yield takeEvery("ADD_NUMBER", addFun); // 被监听到了,对应执行 addFun 函数
}
export function* root() {
  yield all([ // 使用all运行多个Effect,等待监听结果
    watchAdd()
  ]);
}
1
2
3
4
5
6
7
8
9
10
11

# reducer

const defaultState = {
  number: 0
};

export default function reducer(state = defaultState, action) {
  switch (action.type) {
    case "ADD_NUMBER_FOR_REDUCER":
      return { ...state, number: state.number + 1 };
    default:
      return state;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 启动与监听

import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import { root } from "./sagas";
import reducers from "./reducers";
const sagaMiddleware = createSagaMiddleware();
export const store = createStore(reducers, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(root); // 由 sagas.js 导出root进行启动与监听
1
2
3
4
5
6
7

# Api

saga 还内置了很多很实用的api,包括防抖、节流、延迟、取消 等

# 常用辅助函数

  • takeEvery (pattern, saga, ...args)

    • pattern: 需要监听的值 如: “TEST”
    • saga: 需要执行的对应函数
    • args: 需要传给函数的参数
  • takeLatest (pattern, saga, ...args)

    使用方法与takeEvery基本相同,不同的是takeLatest只会执行会后一次任务,如果上一次已经启动就会取消上一次的运行。

  • throttle (ms, pattern, saga, ...args) 节流

# 常用 Effect Api

  • take(pattern)

    while(true) {
        let one = yield take('TEST'yield put({type:'TESTXXX',value}}
    
    1
    2
    3
    4

    take方法类似于一次性使用得所以经常和while搭配,可以保持一直监听得状态,但是又可以有效的控制流程

  • put(action): 创建一个Effect,用来命令中间件向store发起该action,这个action是非阻塞的。相当于dispatch(action)

  • call(fn, ...args) : call方法调用fn,参数为args,返回一个描述对象

  • fork(fn, ...args): 与call 大致相同,但为 非阻塞调用 的形式执行 fn

  • cancel(task): 用来命令 middleware 取消之前的一个任务

  • select(selector, ...args): 用于获取Store 的 state

# Effect 组合器

  • cancel() 取消

    某个任务呗cancel取消时,处理取消逻辑写在 finally

    function* forkTask() {
        try {
            // 2.1,延迟2s控制台打印 'forkTask finished'
            yield delay(2000)
            console.log('forkTask finished');
        } catch (error) {
            // 2.2,如果出错则控制台打印 'error'
            console.log('error');
        } finally {
            // 2.3,forkTask不管以怎样形式结束都将执行finally区块内部内容
            console.log('当task以任意方式结束,不管是正常结束,抛错结束,还是当前任务取消结束,finnally都将执行');
        }
    
    }
    function* cancelFork() {
        // 2,cancelFork使用fork启动一个非阻塞的任务forkTask
        const task = yield fork(forkTask)
        // 3,延迟1s
        yield delay(1000)
        // 4,取消任务forkTask
        yield cancel(task)
    }
    function* rootSaga() {
        // 1,根saga启动cancelFork任务
        yield call(cancelFork)
    }
    export default rootSaga
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
  • all() 并发

    all参数中所有任务全部完成,all所在的Generator函数才会恢复执行。而如果参数中某个任务失败且该任务未对错误进行处理,那么错误将冒泡到all所在的Generator中,且取消其他任务

  • race()

    race方法类似于Promise.race,即race参数中多个任务竞赛,谁先完成,race就结束,这里也分两种情况:1,如果率先完成者正常完成,则取消其他未完成的任务,且完成任务结果时该任务return值,其他取消任务的结果均为undefined。 2,率先完成任务失败(抛错且未处理),则错误冒泡到race所在Generator函数中,且取消其他竞赛中的任务。

# 阻塞与非阻塞

function block
take 阻塞
call 阻塞
all 不一定
putResolve 阻塞
join 阻塞
put 非阻塞
fork 非阻塞
cancel 非阻塞
cps 非阻塞

# 非阻塞并非异步

redux-sagaput()非阻塞意思是假如这个action中有中间件,或一些异步操作造成了store信息更新不及时,那么effects中并不会等着这些操作执行完,即会继续执行接下来的操作

# 总结

redux-saga优点:

  • 异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中
  • action摆脱thunk function:dispatch 的参数依然是一个纯粹的 action (FSA),而不是充满 “黑魔法” thunk function
  • 异常处理:受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
  • 功能强大:redux-saga提供了大量的Saga 辅助函数和Effect 创建器供开发者使用,开发者无须封装或者简单封装即可使用
  • 灵活:redux-saga可以将多个Saga可以串行/并行组合起来,形成一个非常实用的异步flow
  • 易测试:提供了各种case的测试方案,包括mock task,分支覆盖等等

redux-saga缺陷:

  • 额外的学习成本: redux-saga不仅在使用难以理解的 generator function,而且有数十个API,学习成本远超redux-thunk,最重要的是你的额外学习成本是只服务于这个库的,与redux-observable不同,redux-observable虽然也有额外学习成本但是背后是rxjs和一整套思想
  • 体积庞大: 体积略大,代码近2000行,min版25KB左右
  • 功能过剩: 实际上并发控制等功能很难用到,但是我们依然需要引入这些代码
  • ts支持不友好: yield无法返回TS类型

参考:

Redux-Thunk vs. Redux-Saga (opens new window)

Tsuki_ (opens new window)

副作用Effect--维基百科 (opens new window)

Alan He (opens new window)

issues (opens new window)

掘金社区 (opens new window)

Last Updated: 8/17/2021, 6:24:19 PM