redux-saga实践与思考
redux-saga
redux-saga实践与思考(一)
对于redux-saga,网上的文章已经数不胜数,在这里重提redux-saga,主要是想记录一下最近使用redux-saga的一些考虑和看法,本文不会去讲解各个 Effect ,如有需要可查看文档。
在redux-saga官网中的解释是一个saga就像是应用程序中一个单独的线程,独自负责处理副作用。第一次读完这句话,心里并没有太多的思考。因此后续在项目中也爬了许多坑。在这里我们需要明确两点,saga如何像一个单独的线程,在项目中具体是什么样的?什么叫做副作用?
首先来看两段示例代码:
代码一:
``` js
// getInitialData获取初始化数据 handleFetchShopData 获取商品数据
function* initialAct() {
try {
const res = yield call(getInitialData)
const { code, userType } = res
if(code === '0'){
if(userType) {
yield fork(handleFetchShopData, userType)
}
}
} catch(err) {
console.log('initialAct', err)
}
}
function* handleFetchShopData(param) {
// do something
}
export default {
saga: function*() {
yield fork(initialAct)
}
}
```
在上述这段代码中,通过fork调用初始化函数,在初始化接口返回结果之后,拿到userType再去fork执行获取商品的数据,从而达到串行的目的。
从执行结果上来说这段代码并不会出现问题,能达到我们想要的效果。熟悉saga的话,就知道saga是由许多子saga组合起来的,fork即创建一个子saga。如果我们自己的项目中如上述代码一样,在需求改变,fork增多的情况下,我们的函数将变的难以维护,出问题之后不易查找。fork返回结果是一个task对象(包含方法:task.isRunning()、task.result()、task.error()、task.done、task.cancel()),通过join、cancel可以等待之前的fork任务返回结果、取消之前的fork任务。如上写法,fork中将无法做到针对单一fork。代码耦合在一起,不利于后续开发。
代码二:
``` js
// action.js中定义INITIAL_COMPLETE = 'initial_complete'
function initialActComplete(
state = false,
{ type, payload } = {}
) {
switch (type) {
case INITIAL_COMPLETE:
return payload
default:
return state
}
}
function* initialAct() {
try {
const res = yield call(getInitialData)
const { code, userType } = res
if(code === '0'){
if(userType) {
yield put({
type: INITIAL_COMPLETE,
payload: userType
})
}
}
} catch(err) {
console.log('initialAct', err)
}
}
function* handleFetchShopData() {
while (true) {
let { payload } = yield take(INITIAL_COMPLETE);
// do something
}
}
export default {
reducer: {
initialActComplete
},
saga: function*() {
yield fork(initialAct)
yield fork(handleFetchShopData)
}
}
```
上述代码是在优化之后的代码,同时去创建两个task,初始化及获取商品数据。不同的是在第一个task中获取结果后我们发出一条消息INITIAL_COMPLETE,说这个接口已经请求成功了,并且拿到了我们所需要的值。此时这个task的任务已经结束。对于这个消息其他地方是否需要关注,在初始化的task中我们不去考虑。而在第二个获取商品的task中,我们去take INITIAL_COMPLETE,在监听到这个消息拿到之后去获取商品数据。
通过上述方法,可以将代码解耦,每一个子task只存在call或者put、take等,只做这个task自己的事情,不去关注这件事情做完之后还需要做什么,确保task代码的简洁清晰,让业务代码具有更高的可扩展性,即每个saga如一个单独的线程。
副作用
前面讨论了saga如何合理的使用,接下来聊一下什么叫做副作用。这个词在药品中可能更常用,指随着主要作用而附带发生的不好的影响。redux中大量借鉴了Elm语言的设计风格,以及函数式的变成风格,纯函数是FP中的重要核心,FP是什么?用下面一句话来说明:
函数编程就是只使用纯函数与不可改变的值来撰写软体应用的一种方式
同样的在react本身也有这种概念:
所有的React组件必须运作得就像相对于它们props(属性)的纯函数
那什么是纯函数呢?简单的理解就是给一个function相同的参数,永远会返回相同的结果。那在纯函数里什么会算作副作用呢?比如ajax操作,外部数据源读取,跳转外部链接等属于纯函数外的操作均属于副作用。通俗来讲就是一个 function做了和本身运算返回值无关的事。
在react-saga中如何处理异步操作呢?redux-saga中不需要测试副作用函数的返回结果,只需要比较Effect方法后返回的描述对象与期望的描述对象是否相同即可。如下:
``` js
import { call } from 'redux-saga/effects'
function* handleFetchShopData() {
while (true) {
try {
const res = yield call(getInitialData, params)
// do something
} catch(err) {
console.log('handleFetchShopData', err)
}
}
}
```
上述代码中,比如我们需要测试getInitialData返回的结果是否符合预期,通过调用call方法,返回一个描述对象,即所要调用的方法与执行方法时的实参,我们认为只要描述对象相同,那我们执行方法后返回的结果肯定满足预期的结果,这样方便单元测试,不需要模拟接口返回的结果。
补充:在react中,给一个组件一样的props,这个组件永远渲染出相同的视图,即纯组件。纯组件的好处能够容易监测数据变化、容易测试、提高渲染性能等。