React redux - 异步数据流

283 阅读5分钟

原始的Redux里面,action creator必须返回plain object,而且必须是同步的。而在实际业务中往往有大量异步场景,定时器,网络请求等等异步操作。可以使用中间件(middleware)改写 dispatch 方法,因此可以更灵活的控制 dispatch 的时机,这对于处理异步场景非常有效。社区常见的中间件有 redux-thunkredux-promiseredux-sagaredux-observable

// 同步数据
view -> action -> reducer -> store

// 异步数据
view —> action —> middleware —> action(plain) —> reducer —> store

1、redux-thunk

  • 作用
    • 通过拦截处理函数类型的action,通过回调来控制触发普通 action
    • 是一个典型的函数式编程,巧用了闭包,让 dispatch 方法在函子内没有被销毁
  • 缺点:
    • action的形式不统一
    • 异步操作太为分散、不好维护

image.png

组件内使用
// store 对象
const store = createStore(
  reducer,
  // 此处使用 thunk 时传递了额外参数(见源码)
  applyMiddleware(thunk.withExtraArgument({ api, whatever }))
);


// reducer
const reducer = function (oldState, action) {
  switch (action.type) {
    case FETCH_DATA_START:
    // 处理 loading 等
    case FETCH_DATA_SUCCESS:
    // 更新 store 等处理
    case FETCH_DATA_FAILED:
    // 提示异常
  }
};
// action 中调用
const featData = function (id) {
  // 异步数据流
  return function (dispatch, getState) {
    api.fetchData(id)
      .then((response) => {
        // 请求成功时 dispatch
        dispatch({
          type: FETCH_DATA_SUCCESS,
          payload: response,
        });
      })
      .catch((error) => {
        // 请求失败时 dispatch
        dispatch({
          type: FETCH_DATA_FAILED,
          payload: error,
        });
      });
  };
};


// 组件内调用 dispatch,接受一个函数
store.dispatch(featData(88));
源码分析
// 1、简单版本
const thunk = ({ dispatch, getState }) => (next) => (action) => {
  // 判断 action 是否为函数
  if (typeof action === "function") {
    return action(dispatch, getState);
  }

  // 否则按照普通 action 处理
  return next(action);
};


// 2、参数版本
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;

2、redux-promise

redux-promise 直接将这个 promise 作为 action 给 dispatch

组件内使用
//reducer
const reducer = function (oldState, action) {
  switch (action.type) {
    case FETCH_DATA:
      if (action.status === "success") {
        // 更新 store 等处理
      } else {
        // 提示异常
      }
  }
};


//action creator
const getData = function (id) {
  return {
    type: FETCH_DATA,
    payload: api.fetchData(id), // 直接将 promise 作为 payload
  };
};
源码分析
import isPromise from "is-promise";
import { isFSA } from "flux-standard-action";

const promiseMiddleware = ({ dispatch }) => (next) => (action) => {
  // 判断是否是标准的 flux action
  if (!isFSA(action)) {
    return isPromise(action) ? action.then(dispatch) : next(action);
  }

  return isPromise(action.payload) ? 
    action.payload
      .then((result) => dispatch({ ...action, payload: result }))
      .catch((error) => {
        dispatch({ ...action, payload: error, error: true });
        return Promise.reject(error);
      }) : 
      next(action);
};

3、redux-saga

  • 作用
    • 统一形式的 action、集中处理异步操作
    • redux-saga 作用是一个监听器,专门监听 action
    • 声明式易测的 Effects,比如无阻塞调用,中断任务
    • 保持了 action 的原义,保持 action 的简洁,把所有带副作用的地方独立开来
  • 缺点
    • redux-saga 使用的是 generator

image.png

Effect提供的具体方法

import {take,call,put,select,fork,takeEvery,takeLatest} from 'redux-saga/effects'
createSagaMiddleware(options):创建中间件、链接 Saga 到 Redux 的 store 中
  • context: Object - saga 的上下文初始值
  • sagaMonitor : 在派发事件时会通知sagaMonitor
  • onError: (error: Error, { sagaStack: string }) 当 sagas中有未捕获的errors,middleware 会调用这个方法来处理。在追踪错误流时,这个方法很有用
  • effectMiddlewares : Function [] - 这个方法能阻拦任意的 effect,然后将该 effect 传给你自己调用或者传给别的middleware
run:来执行 Sagas,于 applyMiddleware 阶段之后执行 Sagas
take
  • 使用拉取(pull)模式 监听 action
  • 为空或*,那么所有action都会匹配到
  • 阻塞方法 遇到action才会向后执行
import { take, call, put } from 'redux-saga/effects'
import Api from '...'

function* authorize(user, password) {
  try {
    // 调用 authorize 成功、存储 token 、并等待 `LOGOUT` action
    const token = yield call(Api.authorize, user, password)
    yield put({type: 'LOGIN_SUCCESS', token})
    return token
  } catch(error) {
    // 调用 authorize 失败、等待一个新的 `LOGIN_REQUEST` action
    yield put({type: 'LOGIN_ERROR', error})
  }
}

function* loginFlow() {
  // 一旦到达流程最后一步,通过等待一个新的 `LOGIN_REQUEST` action 来启动一个新的迭代
  while(true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    // 1、使用 call 阻塞执行
    // 2、当 LOGIN_REQUEST 没有执行完成时、调用 LOGOUT 会报错
    const token = yield call(authorize, user, password)
    if(token) {
      yield call(Api.storeItem({token}))
      yield take('LOGOUT')
      yield call(Api.clearItem('token'))
    }
  }
}

fork
  • 功能等同于 call、 apply
  • 非阻塞执行
import { isCancelError } from 'redux-saga'
import { fork, call, take, put } from 'redux-saga/effects'
import Api from '...'

function* authorize(user, password) {
  try {
    const token = yield call(Api.authorize, user, password)
    yield put({type: 'LOGIN_SUCCESS', token})
  } catch(error) {
    // 任务被取消
    if(!isCancelError(error)){
      yield put({type: 'LOGIN_ERROR', error})
    }
  }
}

function* loginFlow() {
  while(true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    yield fork(authorize, user, password)
    yield take(['LOGOUT', 'LOGIN_ERROR'])
    // 1、如果 Api 调用过程中,触发了 LOGOUT ACTION
    // 2、需要手动取消,否则将有 2 个并发的任务
    if(action.type === 'LOGOUT'){
      yield cancel(task)
    }
    yield call(Api.clearItem('token'))
  }
}
call、apply
  • 异步请求时使用创建一个纯文本对象描述函数调用
  • 阻塞执行
  • 确保执行函数调用并在响应被 resolve 时恢复 generator
import { call } from 'redux-saga/effects'

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products' , arg1, arg2, ...)
  // ...
}


// 方便测试用例
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)
put
  • 对应 redux 中 dispatch
  • 创建一个将执行异步 action 的任务、通过 reducer 更改 state
import { call, put } from 'redux-saga/effects'

export function* fetchData(action) {
   try {
      const data = yield call(Api.fetchUser, action.payload.url);
      yield put({type: "FETCH_SUCCEEDED", data});
   } catch (error) {
      yield put({type: "FETCH_FAILED", error});
   }
}
takeEvery
  • 监听 action 触发异步任务
  • 允许并发 即同时处理多个相同的 action
  • 为空或*,那么所有action都会匹配到
import { takeEvery } from 'redux-saga'

function* watchFetchData() {
  // 监听 FETCH_REQUESTED 
  yield* takeEvery('FETCH_REQUESTED', fetchData)
}
takeLatest
  • 捕获每一次匹配 pattern 的 action
  • 不允许并发处理中的 action 会被取消,只会执行当前的
  • 为空或*,那么所有action都会匹配到
import { takeEvery } from 'redux-saga'

function* watchFetchData() {
  // 监听 FETCH_REQUESTED 
  yield* takeLatest('FETCH_REQUESTED', fetchData)
}
takeLeading
  • 一旦任务开始,在任务结束前 takeLeading 将不会捕获action
  • 只会在没有执行任务时,才会监听action
const takeLeading = (patternOrChannel, saga, ...args) =>
  fork(function* () {
    while (true) {
      const action = yield take(patternOrChannel);
      yield call(saga, ...args.concat(action));
    }
  }
);
select:对应的是redux中的getState
const id = yield select(state => state.id);
all
  • 并行运行多个Effect
  • 等待他们同时完成
// saga模块化引入
import { fork, all } from "redux-saga/effects";

// 异步逻辑
import { loginSagas } from "./login";

// 单一进入点,一次启动所有Saga
export default function* rootSaga() {
  yield all([fork(loginSagas)]);
}
错误处理
function* fetchProducts() {
  try {
    const products = yield call(Api.fetch, '/products')
    yield put({ type: 'PRODUCTS_RECEIVED', products })
  }
  catch(error) {
    yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
  }
}

3、redux-observable

作用:

  • redux-observable 的核心就是 Epics
  • 收一个 action (plain object) ,返回一个 action 流

image.png

// 安装
yarn add rxjs redux-observable