最近组里的同事在尝试使用 redux-saga 改造一些异步请求比较多的功能。 对 redux-saga 早有耳闻,但是出于懒,一直没去关注,这次就着帮同事做 code review 看了一下 redux-saga 的文档和代码,以下是学习过程中的一些心得和体会。
按照以往学习的思路,一个被众人推崇的东西,一定有这几方面是值得学习和思考的:
以下的体会也将从这三个方面来总结一下
在使用 redux-saga 之前,在 redux 中处理异步请求,通常是使用 redux-thunk 来解决的。
这里多扯一些关于 redux, redux-thunk 和 redux-saga 之间的关系和差异
先来说 redux,redux 本身是一个完整的控制数据流的框架。在 redux 中我们通常关心的是 action, reducer , middleware以及 provider 中的元素如何响应数据的变化。redux 精简了整个流程,使 action 和 reducer 都只需要 return 即可,这样每个部分看起来都可以当做是纯函数,相应的,测试起来也很方便,毕竟是纯函数,固定的输入对应固定的输出。
但是 redux 本身的简单,单纯的 return 并不能处理异步需求。所以才会使用 redux-thunk 这样的插件。
但是需要注意的是,redux-thunk 并不是只为了完成异步需求才会被创造出来的。要理解react-thunk,首先要了解一下 thunk 这个概念
A thunk is a function that wraps an expression to delay its evaluation.
说白了的话,thunk 是一个被延迟执行的函数。
那么redux-thunk是怎么解决异步的问题呢?可以直接看下redux-thunk 的源码,非常非常非常短
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;
之所以 redux-thunk 可以解决异步的问题,是因为将异步的部分包装成了一个 function,并传入了 store.dispatch,这样可以在异步完成之后再调用dispatch 触发 reducer 的中的响应。
所以redux-thunk 的本质是将 action 中返回的函数执行。
相比于redux-thunk,redux-saga 更像是在redux 的middlewaras之外,通过监听 action 提供了一个执行task 的地方。
另外,redux-saga 比较突出的特点是使用了 generator 特性,使异步的返回可以用更流畅的语法来表示。
在一个多交互的页面中,通常会有多个动作触发同一个 action 的时候。比如在购物车中,加/减数量, 选择/取消选择商品, 删除商品都会触发重新计算总价的请求。使用 redux-thunk 的做法是写一个 middleware ,监听所有经过的 action,如果发现 action 是会触发计算总价的请求中的几种,就会去计算总价,等回调完成后再更新总价。
这个时候就容易出现多个计算请求的情况,因为是异步请求,所以其返回顺序不一定,所以更新总价时会造成错误。
如果用 radux-saga 来解决这个问题的话,可以使用takeLastest 来执行最后一次被触发的 task,之前被触发的 task 都会被取消,从而也就解决了这个问题。
当然,这只是一个简单的例子;其实通过其他方法也可以实现在 redux-thunk 中实现取消未完成的异步请求的功能。
使用 redux-saga 的效果可以从以下几个方法来看:
写法上抛弃了 middlewares 的概念,因为 redux-saga本身就是一个middleware,这个 middleware 监听了 action,从而可以根据不同的 action 处理 redux 中的副作用问题。可以简单的理解为将 middleware 全都迁移到了sagas 目录中。
使用了 redux-saga 后,action 中的方法可以变得更纯粹,不需要再在 action 中处理异步问题,每一个 action 只是单纯的返回对应的 type 和相关数据即可。
因为action 变得纯粹,所以为 action 编写测试也就更加方便
异步从 action中专业到了 saga 中,相应单元测试的编写也就从 action 转移到了 saga。但比较方便的是,redux-saga 使用了 generator ,在编写测试用例时,可以基本有同步写法的感觉。
简单的说完了 redux-saga 的一些简介,这里着重列举一下 redux-saga 中的方法和概念。
Effect 是 redux-saga 中的一个简单的对象定义
Effects 是 redux-saga 中对 yield 返回对象的包装。因为redux-saga 中每一个 saga 都是 generator 函数,所以 每个yield返回都可以称作是一个 Effect。
redux-saga 官方推荐使用 redux-saga/effects 中的方法来创建一个 Effect 对象用于返回。但并不是一定要使用其中的方法才能创建 Effect,正如上面所说,每一个yield 返回都可以称之为一个 Effect。
最简单的方式是直接 yield 一个 Promise 对象,也可以通过 call 和 apply 等方式创建 Effect。
但单纯的 yield 一个 Promise 对象和通过 redux-saga/effects 返回一个 Effect 对象是不同的。使用 redux-saga/effects 提供的方法返回的,将是一个纯粹的 javascript 对象,用于描述将要执行的操作,而这个操作,将不会立即执行,而是会到 redux-saga 这个 middleware 中再去执行。
下面我们通过两个例子来说明这两种方式的异同:
// yield a promise
function *getData(url) {
const data = yield Api.Fetch(url)
// ...
}
// yield a effect
import { call } from 'redux-saga/effects'
function *getData(url) {
const data = yield call(Api.Fetch, url)
// ...
}
在针对返回 promise 对象的这个方法写测试时,我们可以这样写:
// unit test for yield promise
const iterator = getData(url)
assert.deepEqual(iterator.next().value, another_promise)
因为 yield 返回的是个 promise 对象,所以在这里我们如果想要测试,也只能创造另一个 promise 对象。 但我们这个单元测试的真正目的是为了测试调用 getData 这个方法,yield 后的方法会不会被正确的执行,这样创造出另一个 promise 对象方法难免有些过于沉重。
再来看下,使用 call 方法返回了 Effect 对象将要怎么测试:
// unit text for yield effect
const iterator = getData(url)
assert.deepEqual(iterator.next().value, call(Api.Fetch, url))
我们只需要再调用一次 Api.Fetch 方法,并不需要关心call 调用的是什么方法,写起测试来,感觉可以更无脑一些。
在 saga 的实际编写中,经常会遇到某个异步请求结束后,需要 dispatch 一个新的事件,以让 store 和 reducer 做出相应 。如果直接通过 dispath 方法传递事件和数据的话,单元测试将会无法覆盖到 dispath 的动作。
所以,redux-saga 提供了put 方法,这个方法类似 call,将会创建一个标准的可描述对象,将 dispatch 的操作放在 redux-saga 的 middleware 中执行。在 saga方法中可以 yield 这个对象,则可以在单元测试中覆盖这个操作。
使用 takeEvery,可以监听每一次 action 的调用,并返回相应的 Effect并执行;与之不同的是 takeLast,将只执行最新的一次action 的调用。
所以,使用 takeLast 可以准确的获取多次异步请求中最新的一次,解决上面所说的购物车问题。