Build Real App with React #14: Redux Saga

Ref: 聊一聊 redux 异步流之 redux-saga  [入门]

[深入]

Ref: Saga中文文档

 

函数式编程范式的“副作用”

在实际的应用开发中,我们希望做一些异步的(如Ajax请求)且不纯粹的操作(如改变外部的状态),这些在函数式编程范式中被称为“副作用”。

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

redux-saga 就是用来处理上述副作用(异步任务)的一个中间件。

它是一个接收事件,并可能触发新事件的过程管理者,为你的应用管理复杂的流程。

 

参考:[React] 12 - Redux: async & middleware

需求:"假如当每次 Button 被点击的时候,我们想要从给定的 url 中获取数据"

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

采用 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 的缺点

(1)action 虽然扩展了,但因此变得复杂,后期可维护性降低;
(2)thunks 内部测试逻辑比较困难,需要mock所有的触发函数;
(3)协调并发任务比较困难,当自己的 action 调用了别人的 action,别人的 action 发生改动,则需要自己主动修改;
(4)业务逻辑会散布在不同的地方:启动的模块,组件以及thunks内部。
// 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: object --> function
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}) }) });

 

 

Saga来了  

[React] 14 - Redux: Redux Saga
 
sages 采用 Generator 函数来 yield Effects(包含指令的文本对象)。
 * Generator 函数的作用是可以暂停执行,再次执行的时候从上次暂停的地方继续执行。
 * Effect 是一个简单的对象,该对象包含了一些给 middleware 解释执行的信息。effects对象的API 例如: forkcalltakeputcancel 等来创建 Effect
 
第一,
// Effect -> 调用 fetch 函数并传递 `./products` 作为参数
{
  type:     CALL,
  function: fetch,
  args:     ['./products']
}

 

第二,

redux-thunk 不同的是,在 redux-saga 中,
UI 组件自身从来不会触发任务,
而是会 dispatch 一个 action 来通知在 UI 中哪些地方发生了改变,
而不需要对 action 进行修改。
 
redux-saga异步任务进行了集中处理,且方便测试。
 
 
第三,
sagas 包含3个部分
  • worker saga
    做所有的工作,如调用 API,进行异步请求,并且获得返回结果
  • watcher saga
    监听被 dispatch 的 actions,当接收到 action 或者知道其被触发时,调用 worker saga 执行任务
  • root saga
    立即启动 sagas 的唯一入口
 
第四,
在 redux-saga 中的基本概念就是:
 (1) sagas 自身不真正执行副作用(如函数 call),但是会构造一个需要执行作用的描述。
 (2) 中间件会执行该副作用并把结果返回给 generator 函数。

 

 

如何使用Saga 

1. 加入 saga 中间件,并且启动它,它会一直运行

//...
import { createStore, applyMiddleware} from 'redux';
import createSagaMiddleware            from 'redux-saga';
import appReducer                      from './reducers';
//...

const sagaMiddleware = createSagaMiddleware();
const middlewares    = [sagaMiddleware];

const store = createStore(appReducer, applyMiddleware(...middlewares));
sagaMiddleware.run(rootSaga);

render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('app')
);

 

2. 在 sagas 文件夹中集中写 saga 文件

import { take, fork, call, put } from 'redux-saga/effects';    // jeff: 都是纯函数,每个函数构造一个特殊的对象

// The worker: perform the requested task function* fetchUrl(url) { const data = yield call(fetch, url); // 指示中间件调用 fetch 异步任务  jeff: data是一个类似于 {type: CALL, function: fetchUrl, args: [url]} 的对象
/**
* 中间件会停止 generator 函数,
* 直到 fetch 返回的 Promise 被 resolved(或 rejected
*/
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
/**
* 中间件会暂停执行 wacthFetchRequests generator 函数,
* 直到 FETCH_REQUEST action 被 dispatch。
*/
yield fork(fetchUrl, action.url); // 指示中间件以无阻塞调用方式执行 fetchUrl /**
* 告诉中间件去无阻塞调用一个新的 fetchUrl 任务,
* action.url 作为 fetchUrl 函数的参数传递。
*/
} }

 

总结领悟:

JavaScript 是单线程的,redux-saga 让事情看起来是同时进行的。

架构上的优势:

将所有的异步流程控制都移入到了 sagas,UI 组件不用执行业务逻辑,只需 dispatch action 就行,增强组件复用性。

 


结合具体使用场景:登录 

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST) //等待 Store 上指定的 action LOGIN_REQUEST
    try {
      let { data } = yield call(request.post, '/login', { user, pass }); // 阻塞,请求后台数据
      yield fork(loadUserData, data.uid);        // 非阻塞执行loadUserData
      yield put({ type: LOGIN_SUCCESS, data });  // 发起一个action,类似于dispatch
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(request.get, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

 

提出一个问题:

Firebase提供的登录接口为何使用起来简单许多,而以上这些方案却相对复杂了许多?

 

相关文章:

  • 2021-09-07
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2021-10-10
  • 2022-12-23
  • 2021-05-22
  • 2022-12-23
猜你喜欢
  • 2019-07-19
  • 2021-10-17
  • 2022-12-23
  • 2018-11-04
  • 2021-10-12
  • 2022-12-23
相关资源
相似解决方案