聊一聊 redux 异步流之 redux-saga

React+Redux Cycle(来源:https://www.youtube.com/watch?v=1QI-UE3-0PU)

让我惊讶的是,redux-saga 的做者居然是一名金融出身的在一家房地产公司工做的员工(让我想到了阮老师。。。),可是他对写代码有着很是浓厚的热忱,喜欢学习和挑战新的事物,并探索新的想法。恩,牛逼的人不须要解释。javascript

1. 介绍

对于历来没有据说过 redux-saga 的人,做者会如何描述它呢?html

It is a Redux middleware for handling side effects. —— Yassine Elouafijava

这里包含了两个信息:ios

首先,redux-saga 是一个 redux 的中间件,而中间件的做用是为 redux 提供额外的功能。git

其次,咱们都知道,在 reducers 中的全部操做都是同步的而且是纯粹的,即 reducer 都是纯函数,纯函数是指一个函数的返回结果只依赖于它的参数,而且在执行过程当中不会对外部产生反作用,即给它传什么,就吐出什么。可是在实际的应用开发中,咱们但愿作一些异步的(如Ajax请求)且不纯粹的操做(如改变外部的状态),这些在函数式编程范式中被称为“反作用”。github

Redux 的做者将这些反作用的处理经过提供中间件的方式让开发者自行选择进行实现。编程

redux-saga 就是用来处理上述反作用(异步任务)的一个中间件。它是一个接收事件,并可能触发新事件的过程管理者,为你的应用管理复杂的流程。json

2. 先说一说 redux-thunk

redux-thunkredux-saga 是 redux 应用中最经常使用的两种异步流处理方式。redux

From a synchronous perspective, a Thunk is a function that has everything already that it needs to do to give you some value back. You do not need to pass any arguments in, you simply call it and it will give you value back.
从异步的角度,Thunk 是指一切都就绪的会返回某些值的函数。你不用传任何参数,你只需调用它,它便会返回相应的值。—— Rethinking Asynchronous Javascriptaxios

redux-thunk 的任务执行方式是从 UI 组件直接触发任务。

举个栗子:

假如当每次 Button 被点击的时候,咱们想要从给定的 url 中获取数据,采用 redux-thunk, 咱们会这样写:

// fetchUrl 返回一个 thunk
function fetchUrl(url) {
  return (dispatch) => {
    dispatch({
      type: 'FETCH_REQUEST'
    });

    fetch(url).then(data => dispatch({
      type: 'FETCH_SUCCESS',
      data
    }));
  }
}

// 若是 thunk 中间件正在运行的话,咱们能够 dispatch 上述函数以下:
dispatch(
  fetchUrl(url)
):

redux-thunk 的主要思想是扩展 action,使得 action 从一个对象变成一个函数。

另外一个较完整的栗子:

// redux-thunk example
import {applyMiddleware, createStore} from 'redux';
import axios from 'axios';
import thunk from 'redux-thunk';

const initialState = { fetching: false, fetched: false, users: [], error: null }
const reducer = (state = initialState, action) => {
    switch(action.type) {
        case 'FETCH_USERS_START': {
            return {...state, fetching: true} 
            break;
        }
        case 'FETCH_USERS_ERROR': {
            return {...state, fetching: false, error: action.payload} 
            break;
        }
        case 'RECEIVE_USERS': {
            return {...state, fetching: false, fetched: true, users: action.payload} 
            break;
        }
    }
    return state;
}
const middleware = applyMiddleware(thunk);

// store.dispatch({type: 'FOO'});
// redux-thunk 的做用便是将 action 从一个对象变成一个函数
store.dispatch((dispatch) => {
    dispatch({type: 'FETCH_USERS_START'});
    // do something async
    axios.get('http://rest.learncode.academy/api/wstern/users')
        .then((response) => {
            dispatch({type: 'RECEIVE_USERS', payload: response.data})
        })
        .catch((err) => {
            dispatch({type: 'FECTH_USERS_ERROR', payload: err})
        })
});

redux-thunk 的缺点:
(1)action 虽然扩展了,但所以变得复杂,后期可维护性下降;
(2)thunks 内部测试逻辑比较困难,须要mock全部的触发函数;
(3)协调并发任务比较困难,当本身的 action 调用了别人的 action,别人的 action 发生改动,则须要本身主动修改;
(4)业务逻辑会散布在不一样的地方:启动的模块,组件以及thunks内部。

3. redux-saga 是如何工做的?

sages 采用 Generator 函数来 yield Effects(包含指令的文本对象)。Generator 函数的做用是能够暂停执行,再次执行的时候从上次暂停的地方继续执行。Effect 是一个简单的对象,该对象包含了一些给 middleware 解释执行的信息。你能够经过使用 effects APIforkcalltakeputcancel 等来建立 Effect。(redux-saga API 参考

yield call(fetch, '/products')yield 了下面的对象,call 建立了一条描述结果的信息,而后,redux-saga middleware 将确保执行这些指令并将指令的结果返回给 Generator:

// Effect -> 调用 fetch 函数并传递 `./products` 做为参数
{
  type: CALL,
  function: fetch,
  args: ['./products']
}

redux-thunk 不一样的是,在 redux-saga 中,UI 组件自身历来不会触发任务,它们老是会 dispatch 一个 action 来通知在 UI 中哪些地方发生了改变,而不须要对 action 进行修改。redux-saga 将异步任务进行了集中处理,且方便测试。

dispacth({ type: 'FETCH_REQUEST', url: /* ... */} );

全部的东西都必须被封装在 sagas 中。sagas 包含3个部分,用于联合执行任务:

  1. worker saga
    作全部的工做,如调用 API,进行异步请求,而且得到返回结果
  2. watcher saga
    监听被 dispatch 的 actions,当接收到 action 或者知道其被触发时,调用 worker saga 执行任务
  3. root saga
    当即启动 sagas 的惟一入口
// example 1
import { take, fork, call, put } from 'redux-saga/effects';

// The worker: perform the requested task
function* fetchUrl(url) {
  const data = yield call(fetch, url);  // 指示中间件调用 fetch 异步任务
  yield put({ type: 'FETCH_SUCCESS', data });  // 指示中间件发起一个 action 到 Store
}

// The watcher: watch actions and coordinate worker tasks
function* watchFetchRequests() {
  while(true) {
    const action = yield take('FETCH_REQUEST');  // 指示中间件等待 Store 上指定的 action,即监听 action
    yield fork(fetchUrl, action.url);  // 指示中间件以无阻塞调用方式执行 fetchUrl
  }
}

redux-saga 中的基本概念就是:sagas 自身不真正执行反作用(如函数 call),可是会构造一个须要执行做用的描述。中间件会执行该反作用并把结果返回给 generator 函数。

对上述例子的说明:

(1)引入的 redux-saga/effects 都是纯函数,每一个函数构造一个特殊的对象,其中包含着中间件须要执行的指令,如:call(fetchUrl, url) 返回一个相似于 {type: CALL, function: fetchUrl, args: [url]} 的对象。

(2)在 watcher saga watchFetchRequests中:

首先 yield take('FETCH_REQUEST') 来告诉中间件咱们正在等待一个类型为 FETCH_REQUEST 的 action,而后中间件会暂停执行 wacthFetchRequests generator 函数,直到 FETCH_REQUEST action 被 dispatch。一旦咱们得到了匹配的 action,中间件就会恢复执行 generator 函数。

下一条指令 fork(fetchUrl, action.url) 告诉中间件去无阻塞调用一个新的 fetchUrl 任务,action.url 做为 fetchUrl 函数的参数传递。中间件会触发 fetchUrl generator 而且不会阻塞 watchFetchRequests。当fetchUrl 开始执行的时候,watchFetchRequests 会继续监听其它的 watchFetchRequests actions。固然,JavaScript 是单线程的,redux-saga 让事情看起来是同时进行的。

(3)在 worker saga fetchUrl 中,call(fetch,url) 指示中间件去调用 fetch 函数,同时,会阻塞fetchUrl 的执行,中间件会中止 generator 函数,直到 fetch 返回的 Promise 被 resolved(或 rejected),而后才恢复执行 generator 函数。

另外一个栗子

// example 2
import { takeEvery } from 'redux-saga';
import { call, put } from 'redux-saga/effects';
import axois from 'axois';

// 1. our worker saga
export function* createLessonAsync(action) {
    try {
        // effects(call, put): 
        // trigger off the code that we want to call that is asynchronous 
        // and also dispatched the result from that asynchrous code.
        const response = yield call(axois.post, 'http://jsonplaceholder.typicode.com/posts', {section_id: action.sectionId});
        yield put({type: 'lunchbox/lessons/CREATE_SUCCEEDED', response: response.data});
    } catch(e) {
        console.log(e);
    }
}

// 2. our watcher saga: spawn a new task on each ACTION
export function* watchCreateLesson() {
    // takeEvery: 
    // listen for certain actions that are going to be dispatched and take them and run through our worker saga.
    yield takeEvery('lunchbox/lessons/CREATE', createLessonAsync);
}


// 3. our root saga: single entry point to start our sagas at once
export default function* rootSaga() {
    // combine all of our sagas that we create
    // and we want to provide all our Watchers sagas
    yield watchCreateLesson()
}

最后,总结一下 redux-saga 的优势:

(1)声明式 Effects:全部的操做以JavaScript对象的方式被 yield,并被 middleware 执行。使得在 saga 内部测试变得更加容易,能够经过简单地遍历 Generator 并在 yield 后的成功值上面作一个 deepEqual 测试。
(2)高级的异步控制流以及并发管理:可使用简单的同步方式描述异步流,并经过 fork 实现并发任务。
(3)架构上的优点:将全部的异步流程控制都移入到了 sagas,UI 组件不用执行业务逻辑,只需 dispatch action 就行,加强组件复用性。

4. 附上测试 demo

redux-async-demo

5. 参考

redux-saga - Saga Middleware for Redux to Handle Side Effects - Interview with Yassine Elouafi
redux-saga 基本概念
Redux: Thunk vs. Saga
从redux-thunk到redux-saga实践
React项目小结系列:项目中redux异步流的选择
API calls from Redux 系列

相关文章
相关标签/搜索