Redux 异步数据管理

目录

前言

一、redux-thunk 中间件

二、redux-promise 中间件

三、redux-saga 中间件(推荐)


前言

Redux 只处理同步数据流,异步数据流交给其 “中间件” 处理。

Redux 异步操作的中间件经历了三次发展:

Redux 异步操作的基本思路:

  • 操作开始时,送出一个 Action,触发 State 更新为"正在操作"状态,View 重新渲染。
  • 操作结束后,再送出一个 Action,触发 State 更新为"操作结束"状态,View 再一次重新渲染。

Redux 可以直接调用 dispatch 来处理异步数据流,但是不够优雅。下面拿两个案例对比着进一步说明:

【案例一】:不使用中间件,直接调用 dispatch

// action creator
function loadData(dispatch, userId) { // 需要调度,所以这是第一个参数
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())f
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  loadData(this.props.dispatch, this.props.userId); // 别忘了通过调度
}

【案例二】:使用 redux-thunk 中间件

// action creator
function loadData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk处理这些
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  this.props.dispatch(loadData(this.props.userId));
}

对比上述两个案例可知:中间件只关心 dispatch 的传递,并不限制你做其他的事情。thunk 函数只是更改了你的写法——不使用 thunk 你需要每次传递给异步函数 dispatch,使用 thunk 函数后在组件中就可以调用 dispatch,组件不再需要关注 dispatch 派发的函数是不是异步,组件只是发出一个请求而已。

一、redux-thunk 中间件

redux-thunk 的基本思想就是通过函数来封装异步请求,也就是说在actionCreater中返回一个函数,在这个函数中进行异步调用。
redux 中间件只关注 dispatch 函数的传递,而且redux 也不关心 dispatch 函数的返回值。

redux-thunk 的源码(node_modules/redux-thunk/src/index.js):

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;

解析成 ES5 的代码如下:

function createThunkMiddleware(extraArgument) {
    return function({ dispatch, getState }) {
        return function(next){
            return function(action){
                if (typeof action === 'function') {
                    return action(dispatch, getState, extraArgument);
                }
                return next(action);
            };
        }
    }
}

由上述代码可知:redux-thunk 在 actionCreater 中返回一个函数,在这个函数中进行异步调用。如果这个 actionCreater 传过来的action是一个函数的话,就执行它,并以这个函数的返回值作为返回值;如果不是,就按照原来的 next(action) 执行。

redux-thunk 的缺点:

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

二、redux-promise 中间件

阮一峰老师在《Redux 入门教程(二):中间件与异步操作》写到:

既然 Action Creator 可以返回函数,当然也可以返回一个 Promise 对象,这就是 redux-promise 中间件。redux-promise 中间件就是 redux-thunk 与 promise 的结合。


import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';

const store = createStore(
  reducer,
  applyMiddleware(promiseMiddleware)
); 

这个中间件使得 store.dispatch 方法可以接受 Promise 对象作为参数。这时,Action Creator 有两种写法:

写法一:返回值是一个 Promise 对象。

const fetchPosts = 
  (dispatch, postTitle) => new Promise(function (resolve, reject) {
     dispatch(requestPosts(postTitle));
     return fetch(`/some/API/${postTitle}.json`)
       .then(response => {
         type: 'FETCH_POSTS',
         payload: response.json()
       });
});

写法二:Action 对象的 payload 属性是一个 Promise 对象。这需要从 redux-actions 模块引入 createAction 方法,并且写法也要变成下面这样。

import { createAction } from 'redux-actions';

class AsyncApp extends Component {
  componentDidMount() {
    const { dispatch, selectedPost } = this.props
    // 发出同步 Action
    dispatch(requestPosts(selectedPost));
    // 发出异步 Action
    dispatch(createAction(
      'FETCH_POSTS', 
      fetch(`/some/API/${postTitle}.json`)
        .then(response => response.json())
    ));
  }

上面代码中,第二个 dispatch 方法发出的是异步 Action,只有等到操作结束,这个 Action 才会实际发出。注意,createAction 的第二个参数必须是一个 Promise 对象。

看一下redux-promise 的源码,就会明白它内部是怎么操作的。

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action)
        ? action.then(dispatch)
        : next(action);
    }

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

从上面代码可以看出,如果 Action 本身是一个 Promise,它 resolve 以后的值应该是一个 Action 对象,会被dispatch方法送出(action.then(dispatch)),但 reject 以后不会有任何动作;如果 Action 对象的payload属性是一个 Promise 对象,那么无论 resolve 和 reject,dispatch方法都会发出 Action。

三、redux-saga 中间件(推荐)

Redux-saga 官网:自述 · Redux-Saga
Redux-saga 的 API:API 参考 · Redux-Saga

redux-saga 用 generator 代替了 promise(babel的基础版本不包含generator语法,因此需要在使用saga的地方import ‘babel-polyfill’)。

redux-saga 将异步任务进行了 集中处理。

sagas 包含3个部分,用于联合执行任务:

  • worker saga :做所有的工作,如调用 API,进行异步请求,并且获得返回结果。
  • watcher saga :监听被 dispatch 的 actions,当接收到 action 或者知道其被触发时,调用 worker saga 执行任务。
  • root saga :立即启动 sagas 的唯一入口。

redux-saga 的使用方法:

  • 使用createSagaMiddleware方法创建saga 的Middleware,然后在创建的redux的store时,使用applyMiddleware函数将创建的sagaMiddleware实例绑定到store上,最后可以调用saga Middleware的run函数来执行某个或者某些Middleware。
  • 在saga的Middleware中,可以使用takeEvery或者takeLatest等API来监听某个action,当某个action触发后,saga可以使用call、fetch等api发起异步操作,操作完成后使用put函数触发action,同步更新state,从而完成整个State的更新。

首先需要启动saga,启动saga一般都写在入口文件中,下面是个栗子:

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

const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
const store = createStore(appReducer,applyMiddleware(...middlewares));

sagaMiddleware.run(rootSaga);//saga一旦执行就会永远执行下去

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

然后,就可以在 sagas 文件夹中集中写 saga 文件了:

import { take, fork, call, put } from 'redux-saga/effects';
//执行函数即work saga
function* fetchUrl(url) {
	try{//利用try-catch来捕获异常
		const data = yield call(fetch, url); // 指示中间件调用 fetch 异步任务
		yield put({ type: 'FETCH_POSTS_SUCCESS', payload:data }); // 指示中间件发起一个 action 到 Store
	}catch(e){
		yield put({ type: 'FETCH_POSTS_FAILURE', payload:error })
	}
}
// 监听函数即watch saga
function* watchFetchRequests() {
	while(true) {
		const action = yield take('FETCH_POSTS_REQUEST'); // 指示中间件等待 Store 上指定的 action,即监听 action
		yield fork(fetchUrl, action.url); // 指示中间件以无阻塞调用方式执行 fetchUrl
	}
}

在 redux-saga 中的基本概念就是:sagas 自身不真正执行副作用(如函数 call),但是会构造一个需要执行副作用的描述。中间件会执行该副作用并把结果返回给 generator 函数。
对于sages ,采用 Generator 函数来 yield Effects(包含指令的文本对象)。Generator 函数的作用是可以暂停执行,再次执行的时候从上次暂停的地方继续执行。Effect 是一个简单的对象,该对象包含了一些给 middleware 解释执行的信息。你可以通过使用 effects API 如 fork,call,take,put,cancel 等来创建 Effect。

关于Effect官方是这样解释的:

在 redux-saga 的世界里,Sagas 都用 Generator 函数实现。我们从 Generator 里 yield 纯 JavaScript 对象以表达 Saga 逻辑。 我们称呼那些对象为 Effect。Effect 是一个简单的对象,这个对象包含了一些给 middleware 解释执行的信息。 你可以把 Effect 看作是发送给middleware 的指令以执行某些操作(调用某些异步函数,发起一个 action 到 store)。

对上述例子的说明:

  • 引入的 redux-saga/effects 都是纯函数,每个函数构造一个特殊的对象,其中包含着中间件需要执行的指令,如:call(fetchUrl, url) 返回一个类似于 {type: CALL, function: fetchUrl, args: [url]} 的对象。
  • 在 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 让事情看起来是同时进行的。
  • 在 worker saga fetchUrl 中,call(fetch,url) 指示中间件去调用 fetch 函数,同时,会阻塞fetchUrl 的执行,中间件会停止 generator 函数,直到 fetch 返回的 Promise 被 resolved(或 rejected),然后才恢复执行 generator 函数。

这里我们可以来看一波源码,搞清楚这个Effect到底是什么。
对于Effect对象的定义,写在了 redux-saga/src/internal/io.js 文件中,下面是Effect的定义。

const effect = (type, payload) => ({ [IO]: true, [type]: payload });

很简单就一句话,表明这个effect其实就是返回了一个对象。

接下来看看所谓的put和call到底是个什么东西:

put

export function put(channel, action) {
	if (process.env.NODE_ENV === 'development') {
		if (arguments.length > 1) {
			check(channel, is.notUndef, 'put(channel, action): argument channel is undefined')
			check(channel, is.channel, `put(channel, action): argument ${channel} is not a valid channel`)
			check(action, is.notUndef, 'put(channel, action): argument action is undefined')
		} else {
			check(channel, is.notUndef, 'put(action): argument action is undefined')
		}
	}
	if (is.undef(action)) {
		action = channel
		channel = null
	}
	return effect(PUT, { channel, action })
}

call

function getFnCallDesc(meth, fn, args) {
	if (process.env.NODE_ENV === 'development') {
		check(fn, is.notUndef, `${meth}: argument fn is undefined`)
	}
	let context = null
	if (is.array(fn)) {
		[context, fn] = fn
	} else if (fn.fn) {
		({ context, fn } = fn)
	}
	if (context && is.string(fn) && is.func(context[fn])) {
		fn = context[fn]
	}
	if (process.env.NODE_ENV === 'development') {
		check(fn, is.func, `${meth}: argument ${fn} is not a function`)
	}
	return { context, fn, args }
}
export function call(fn, ...args) {
	return effect(CALL, getFnCallDesc('call', fn, args))
}

出乎意料都是只返回了一个纯对象( 先不管细节 )。
effect返回的纯对象由于generate函数的机制会将yield的控制权交给外部,用来给generator外层的执行容器task( 这东西我讲不清楚所以就不讲了 )发送一个信号,告诉task该做什么。task在接收到effect发出的指令后将会执行下面这段函数。

function next(arg, isErr) {
	// Preventive measure. If we end up here, then there is really something wrong
	if (!mainTask.isRunning) {
		throw new Error('Trying to resume an already finished generator')
	}
	try {
		let result
		if (isErr) {
			result = iterator.throw(arg)
		} else if (arg === TASK_CANCEL) {
			/**
			getting TASK_CANCEL automatically cancels the main task
			We can get this value here
			- By cancelling the parent task manually
			- By joining a Cancelled task
			**/
			mainTask.isCancelled = true
			/**
			Cancels the current effect; this will propagate the cancellation down to any called tasks
			**/
			next.cancel()
			/**
			If this Generator has a `return` method then invokes it
			This will jump to the finally block
			**/
			result = is.func(iterator.return) ? iterator.return(TASK_CANCEL) : { done: true, value: TASK_CANCEL }
		} else if (arg === CHANNEL_END) {
			// We get CHANNEL_END by taking from a channel that ended using `take` (and not `takem` used to trap
			End of channels)
			result = is.func(iterator.return) ? iterator.return() : { done: true }
		} else {
			result = iterator.next(arg)//这里将会执行generator并将结果赋值给result
		}
		if (!result.done) {//这里会判断这个generator是否执行完毕
			runEffect(result.value, parentEffectId, '', next) //这里的runEffect就是各种执行结果的返回(全部流程到此结束)
		} else {
			/**
			This Generator has ended, terminate the main task and notify the fork queue
			**/
			mainTask.isMainRunning = false
			mainTask.cont && mainTask.cont(result.value)
		}
	} catch (error) {
		if (mainTask.isCancelled) {
			log('error', `uncaught at ${name}`, error.message)
		}
		mainTask.isMainRunning = false
		mainTask.cont(error, true)
	}
}

Redux-saga优点:

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

【推荐阅读】

Redux异步方案选型

Redux异步处理之Redux

JS 运行机制——同步与异步

【参考文章】

《Redux 入门教程(二):中间件与异步操作》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值