1、redux-saga
是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。
换句话说:
- redux-saga是一个redux的中间件,而中间件的作用就是为redux提供额外的作用。
- 在reducers中所有的操作都是同步并且是纯粹的,即reducer都是纯函数,纯函数就是指一个函数的返回结果只依赖它的参数,并且在执行过程中不会对外部产生副作用,即给他传什么,就吐出什么
- 但是在实际的应用开发中,我们希望做一些异步的(如ajax请求)且不纯粹的操作(如改变外部的状态),这些在函数式编程中被称为“副作用”。
- redux-saga就是用来处理上述副作用(异步任务)的一个中间件,他是一个接收事件,并可能触发新事件的过程管理者,为你的应用管理复杂的流程
2、redux-saga工作原理
- saga采用generator函数来yield Effects(包含指令的文本对象)
- Generator函数的作用是可以暂停执行,再次执行的时候可以从上次暂停的地方继续执行
- Effect是一个简单的对象,该对象包含了一些给middleware解释执行的信息
- 你可以通过使用effects Api 如 fork,call,take,put,cancel等来创建Effect
3、redux-saga的分类
- worker saga做实际的工作,如调用API,进行异步请求,获取异步封装的结果
- watcher saga监听被dispatch的actions,当接收到action或者知道其被触发时,调用worker执行任务
- root saga 是启动saga的唯一入口
为了能跑起 Saga,我们需要使用 redux-saga 中间件将 Saga 与 Redux Store 建立连接,下面,我们来用代码来做相应的示例:
首先安装saga
npm install --save redux-saga
然后在redux的index文件中引入:
import { createStore, applyMiddleware } from 'redux';
//引入redux-saga的中间件
import createSagaMiddleWare from 'redux-saga';
import reducers from './reducers';
import rootSaga from './saga/index';
// 执行中间件函数
let sagaMiddleWare = createSagaMiddleWare();
//通过应用中间件的方式进行引用saga
const store = applyMiddleware(sagaMiddleWare)(createStore)(reducers);
//执行根saga
sagaMiddleWare.run(rootSaga);
export default store;
然后在redux目录中建立一个saga目录:
在这个文件中,我们就可以去写saga的相关代码了,我们先写一个基本的saga案例:
因为需要在创建厂库的时候我们需要使用run方法去执行一个根saga,所以我们需要导出一个saga方法
// => 会立即打印出 hello saga
export default function* (){
console.log('hello saga');
}
这样子,一个基本的saga就完成了,但是只是这样子肯定还是远远不够的,下面,我们就先来说说redux-saga常用的几个方法吧:
import {take,call,put,select,fork,takeEvery,takeLatest,cps} from 'redux-saga/effects'
take方法
take这个方法,是用来监听action,返回的是监听到的action对象,take方法类似于一次性使用。
例如:
const watch2 = function* (){
let actions = yield take(types.ADD);
console.log('动作:',actions)
}
//监听saga
export default function* rootSaga() {
yield all([
watch2(),
helloSaga(),
watchIncrementAsync()
])
}
每次我派发了Add这个动作之后,都会执行watch2这个saga,并且打印出
// => 动作: {type: “ADD”, payload: 20}
call方法
call用来调用异步函数,将异步函数和函数参数作为call函数的参数传入,返回一个js对象。saga引入他的主要作用是方便测试,同时也能让我们的代码更加规范化。
import { call } from 'redux-saga/effects'
function* search() {
const result = yield call(Api.fetch, '/result') // 不传this
//const result = yield call([this, 参数列表...]) // 传this
// ...
}
put方法
put方法的作用主要是用来派发一个action,类似于dispatch方法的作用:
const workSaga = function* (){
let msg = yield delay(3000);
// 派发一个动作
yield put({type: types.DECREMENT,payload:100})
}
select方法
put方法与redux中的dispatch相对应,同样的如果我们想在中间件中获取state,那么需要使用select。select方法对应的是redux中的getState,用户获取store中的state,使用方法:
const workSaga = function* (){
//可以获取到store中的状态
let state = yield select();
console.log('saga state:',state)
let msg = yield delay(3000);
// 派发一个动作
yield put({type: types.DECREMENT,payload:100})
}
fork方法
非阻塞任务调用机制:上面我们介绍过call可以用来发起异步操作,但是相对于generator函数来说,call操作是阻塞的,只有等promise回来后才能继续执行,而fork是非阻塞的 ,当调用fork启动一个任务时,该任务在后台继续执行,从而使得我们的执行流能继续往下执行而不必一定要等待返回。
const delay2 = function(ms){
return new Promise((resolve,reject) => {
setTimeout(()=>{
//后打印
console.log('xxxx')
},ms)
})
}
const testWork = function* (){
yield fork(delay2,2000);
console.log('last') // => 会先打印,fork不会阻塞流程,call会
}
takeEvery方法
takeEvery用于监听相应的动作并执行相应的方法,只要监听到对应动作,那么他就会一直执行对应的监听函数,不像take方法只会执行一次,并且,它还可以同时监听到多个相同的action。
const watch2 = function* (){
// 可以监听多个动作,只要监听到ADD方法,那么对应的后面的函数就会去执行
yield takeEvery(types.ADD,testWork)
yield takeEvery(types.ADD,testWork2)
}
takeLatest方法
takeLatest方法跟takeEvery是相同方式调用,与takeEvery不同的是,takeLatest是会监听执行最近的那个被触发的action。
在实际的项目开发中来我们只需要按照如下的方式来使用redux-saga:
import { all, put, takeEvery } from 'redux-saga/effects';
import * as types from './../action-types';
//工作saga
const workSaga = function* (){
// 派发一个动作
yield put({type: types.DECREMENT,payload:100})
}
//监听saga
const watchIncrementAsync = function* (){
// 当监听到ADD这个动作发生的时候,会派发一个新的动作
yield takeEvery(types.ADD,workSaga)
}
//导出一个根saga
export default function* rootSaga() {
yield all([
watchIncrementAsync()
])
}
cps方法
同call方法基本一样,但是用处不太一样,call一般用来完成异步操作,cps可以用来完成耗时比较长的io操作等。
单元测试
如果想测试一下我们写的saga的功能,也可以写一些单元测试去测试,如下:
首先,安装相关包
npm i @babel/core @babel/node @babel/plugin-transform-modu
les-commonjs --save-dev
* @babel/core 转换es6的语法
* @babel/node 转换node环境下的es6语法
* @babel/plugin-transform-modules-commonjs => babel插件,让node来支持es6的写法
然后在package.json文件中配置一个测试命令:
然后我们开始写我们的单元测试
1、比如我要测试我的saga输出结果是不是会和单元测试中预想的结果一样,如下:
测试代码:
test('incrementAsync saga test',function(assert){
let gen = incrementAsync();
// 第一次不会相等
//测试是否相等 前两个参数是相比较的两个值,第三个参数是测试错误的提示
assert.deepEqual(gen.next().value,call(delay , 3000),'the result will be true after two seconds '); // fail
// 第二次应该相等
assert.deepEqual(gen.next().value,put({type: types.ADD,payload: 1000}),'the second result will be equal '); // pass
//结束
assert.end()
})
saga:
import { put } from '@redux-saga/core/effects';
import * as types from './../action-types';
import { delay } from './../../utils/index';
export function* incrementAsync(){
let msg = yield delay(2000);
console.log('msg:',msg);
yield put({type: types.ADD,payload: 1000})
}
我们在终端中运行
npm run test
最后得到的结果是:
表示的当前的测试失败了,说明我们写的测试函数或者代码有问题。
2、我们也可以写一个方法去模拟node中的readFile方法
// 模拟node中的方法,读取文件
export const readFile = function(fileName,callback){
setTimeout(()=>{
callback(null,fileName + ':content');
},1000)
}
然后写一个saga来使用这个函数:
使用cps来获取回调中的值
export function* readAsync(){
let content = yield cps(readFile,'README.md');
// cps 会获取到执行方法回调函数中的值
console.log(content) // =>打印出 README.md: content
}
然后写一个单元测试来测测这个功能:
test('cps execute readFile',function(assert){
let gen = readAsync();
assert.deepEqual(gen.next().value,cps(readFile,'README.md'),'the readFile result will be equal '); // pass
assert.deepEqual(gen.next(),{value: undefined,done:true},'should done '); // pass
//结束
assert.end()
})
最后全部单元测试的运行结果为: