前言
- 写这玩意差点让我的惰性发挥到极致,不知道从哪里写好,搞得我都不想写。但是只能逼着自己一步步写才能进步。。。
redux-saga是什么
- redux-saga本质上来说是redux的中间件,这里从上篇redux文章可以发现,redux的中间件是通过applyMiddleware实现的,这个函数本质是通过compose来组合一个复杂函数,改写store.dispatch。
- reducer是一个纯函数,拿上一篇的reducer做例子:
function reducer1(state={count:5},action){//初始值分配到每个reducer上使用
switch (action.type){
case 'ADD1':
return {
...state,count:state.count+action.payload
};
case 'MINUS1':
return {
...state,count:state.count-action.payload
};
}
return state
}
- 它的返回结果只依赖于参数,不会产生额外的副作用。
- 但在实际工作中,我们需要做一些异步操作(比如请求)或者不纯粹的操作(改变外部状态)。这些在函数式编程范式中被称为“副作用”。
- redux-saga 就是用来处理副作用(异步任务)的一个中间件。它是一个接收事件,并可能触发新事件的过程管理者,为你的应用管理复杂的流程。
分类
- worker saga 做实际的工作,如调用API,进行异步请求,获取异步封装结果。
- watcher saga 监听被dispatch的actions,当接受到action或者知道其被触发时,调用worker执行任务。
- root saga saga的唯一入口。
使用
- 官网文档
- 先使用create-react-app创建项目。
- 此外还要装这些:
cnpm i redux react-redux redux-saga --save
import React from 'react';
import ReactDOM from 'react-dom'
import { createStore,applyMiddleware,combineReducers } from 'redux'
import createSagaMiddleware from 'redux-saga'
let sagaMiddleware=createSagaMiddleware()
function counter(state = {number:0}, action) {
switch (action.type) {
case 'ADD':
return {...state,number:state.number+1}
case 'MINUS':
return {...state,number:state.number-1}
default:
return state
}
}
let combinereducer =combineReducers({
counter
})
let store = applyMiddleware(sagaMiddleware)(createStore)(combinereducer)
function *rootsaga(){
console.log('yehuozhili');
}
sagaMiddleware.run(rootsaga)
ReactDOM.render(<p>cxzxc</p>,document.getElementById('root'))
- 启动项目,控制台有输出就可以了。
- 然后进行修改,做一个counter组件,实现加一功能。
Counter.js
import React from 'react'
import {connect}from 'react-redux'
import actions from './actions'
function Counter(props) {
return (
<div>
<div>{props.number}</div>
<button onClick={()=>props.add()}>+</button>
</div>
)
}
export default connect(state=>state.counter, actions)(Counter)
actions.js
export default{
add(){
return {type:'ADD'}
}
}
index.js
import React from 'react';
import ReactDOM from 'react-dom'
import { createStore,applyMiddleware,combineReducers } from 'redux'
import createSagaMiddleware from 'redux-saga'
import Counter from './counter'
import {Provider} from 'react-redux'
let sagaMiddleware=createSagaMiddleware()
function counter(state = {number:0}, action) {
switch (action.type) {
case 'ADD':
return {...state,number:state.number+1}
case 'MINUS':
return {...state,number:state.number-1}
default:
return state
}
}
let combinereducer =combineReducers({
counter
})
let store = applyMiddleware(sagaMiddleware)(createStore)(combinereducer)
function *rootsaga(){
console.log('yehuozhili');
}
sagaMiddleware.run(rootsaga)
ReactDOM.render(
<Provider store = {store}>
<Counter></Counter>
</Provider>
,document.getElementById('root'))
- 点击按钮可以正常加一即可。
- 下面就实现异步加一。正常来说,action里只能返回一个对象,如果用thunk中间件,可以返回函数,从而能实现异步。但是saga依然让action返回对象,派发的动作并没有传给reducer,而是被中间件拦截,在中间件中进行执行。中间件中会有一些effect,其中put可以派发action给reducer。
- 这里逻辑就是这样:在saga中间件中监听异步加一的action,监听到后执行workersaga,也就是我们自定义的一个generator函数,这个函数用来代替以前需要改写reducer里对action的响应。在这个函数里,使用delay进行延迟,相当于一些异步操作,然后用put派发加一的action给reducer,从而实现异步加一。
actions.js
export default{
add(){
return {type:'ADD'}
},
delayAdd(){
return {type:'DELAYADD'}
}
}
index.js
import React from 'react';
import ReactDOM from 'react-dom'
import { createStore,applyMiddleware,combineReducers } from 'redux'
import createSagaMiddleware from 'redux-saga'
import Counter from './counter'
import {Provider} from 'react-redux'
import {takeEvery,delay,put,all} from 'redux-saga/effects'
let sagaMiddleware=createSagaMiddleware()
function counter(state = {number:0}, action) {
switch (action.type) {
case 'ADD':
return {...state,number:state.number+1}
case 'MINUS':
return {...state,number:state.number-1}
default:
return state
}
}
let combinereducer =combineReducers({
counter
})
let store = applyMiddleware(sagaMiddleware)(createStore)(combinereducer)
function * workerADD(){//workersaga 有点像reducer的逻辑。实际是被中间件拦截后走的逻辑。
yield delay(1000)
yield put({type:'ADD'})
}
function * watcherDelayAdd(){//watchersaga 用来监听action
yield takeEvery('DELAYADD',workerADD)//监听action,监听到执行worker
}
function *rootsaga(){//入口saga
yield all([watcherDelayAdd()])//all里的saga会全部执行
}
sagaMiddleware.run(rootsaga)
ReactDOM.render(
<Provider store = {store}>
<Counter></Counter>
</Provider>
,document.getElementById('root'))
counter.js
import React from 'react'
import {connect}from 'react-redux'
import actions from './actions'
function Counter(props) {
return (
<div>
<div>{props.number}</div>
<button onClick={()=>props.add()}>+</button>
<button onClick={()=>props.delayAdd()}>异步+</button>
</div>
)
}
export default connect(state=>state.counter, actions)(Counter)
- 实际项目中,每个模块有自己的watcher和worker,导出后,最后统一写在总的rootsaga的all里。
- 还有个传参,workersaga会收到被拦截的action,所以参数只要让action携带,就可以传到worker去。
function * workerADD(action){
yield delay(1000)
yield put({type:'ADD'})
}
- 通常在generator里,yield会阻塞执行,这个监听实际上是在run的时候就已经完成了的。可以试试改变监听的watchersaga,让它停5秒再监控,会发现在载入页面时候确实停了5秒,而且这个时候去点异步加一无效。等下面的sdsa打印在控制台后,点击异步加一会生效。
function * watcherDelayAdd(){
console.log('xxzc');
yield delay(5000)
yield takeEvery('DELAYADD',workerADD)//监听action,监听到执行worker
console.log('sdsa');
}
- 这个意思就是watcher的监控saga会一开始把逻辑全都做好,实际点异步加一并不会走watcher,而直接走worker。
- saga里每个effect里全都是返回一个对象。在run里会对这些对象进行判断和操作。
- 下面实现异步加三次的操作,实际只要把监听函数改成下面即可:
function * watcherDelayAdd(){
yield take('DELAYADD')//监听action,监听到执行worker
yield workerADD()
yield take('DELAYADD')//监听action,监听到执行worker
yield workerADD()
yield take('DELAYADD')//监听action,监听到执行worker
yield workerADD()
}
- 实际工作中会有登录注销,我们对其稍微改一下,模拟登录和注销:
function mylogin (user){
return new Promise((resolve,reject)=>{
setTimeout(() => {
let token = 11
resolve(token)
}, 1000);
})
}
function * workerADD(action){
let token = yield call(mylogin,action.payload)
return token
}
function * watcherDelayAdd(){
while(true){
let action =yield take('LOGIN')
let token = yield workerADD(action)
if(token){
yield put({type:'ADD'})//登录成功,加一
yield take('MINUS')//登录状态退出,减一
}
}
-
这个watcher里面写个while true可以发现,每次退出之后还可以登录,不加的话,注销后就不能登录了,只能登录一次。
-
异步调用和取消任务:
function mylogin (user){
return new Promise((resolve,reject)=>{
setTimeout(() => {
let token = 11
resolve(token)
}, 2000);
})
}
function * workerADD(action){
let token = yield call(mylogin,action.payload)
if(token){
yield put({type:'ADD'})//登录成功,加一
yield take('MINUS')//登录状态退出,减一
}
}
function * watcherDelayAdd(){
while(true){
let action =yield take('LOGIN')
let task = yield fork(workerADD,action)
action = yield take(['LOGIN','MINUS'])//
if(action.type==='MINUS'){//在异步发生时,监听到其中一个action,
yield cancel(task)//对其处理,可以取消这个异步任务
}
}
}
- 这里需要fork这个非阻塞effect,既然异步,所以没有返回值,只会返回task,通过task可以取消这个任务。
- 实际项目中,有很多需要取消任务的情况,比如提交个表单会拉取数据,在等待期间用户不想等了,需要修改下再提交,就可以这么做。
- 另外,task里失败的action也可以放到数组里,然后走后续逻辑,形成闭环。在请求前后,可以设置个flag,这个可以自由发挥。
- 下面利用race,做个数字一直加,监听到特定action后停止:
function * workerADD(){
while(true){
yield delay(1000)
yield put({type:'ADD'})
}
}
function * watcherDelayAdd(){
yield race( [ call(workerADD),take('MINUS')])
}
- 这个race可以放数组或者对象,会让里面东西全部执行,在worker里由于死循环,所以就一直在加一,而另一个take,则是监听minus的action,一旦触发这个action,则其为胜利者,另一个由于死循环无法胜出,就会被取消执行。