thunk及saga中间件使用指南大全

10 篇文章 0 订阅
3 篇文章 0 订阅


相关文章
redux基础


Redux中间件

一般使用中间件的原因都是为了处理异步操作(通常是指在action中执行ajax请求)

异步action(因为要保证reducer的纯净,因此不能外调api,所以在action里处理)

运行原理:actionCreator => 自动dispatch(actionCreator()) => reducer => store => view

// 当使用了connect,进行异步操作时,因为connect会立即dispatch设置的action,但是当异步return action时,就无法在第一时间找到dispatch的action,因此会报错,不能这样写
const action = () => {  
    setTimeout(() => {
        return {
            type: xxx,       
        }
    },2000)
}

redux 中的 action 仅支持原始对象,处理有副作用的action,需要使用中间件,中间件可以在发出action,到reducer函数接受action之间,执行具有副作用的操作

为了能实现能获取到异步返回的最终action数据,需要使用redux的中间件middeware进行处理


加载中间件

使用redux提供的 applyMiddleware 加载指定中间件

import { applyMiddleware } from 'redux'

作用:加载 middleware,将 action 进行多层组合,并且将dispatch和getState方法传入到 action 中,使 action 能通过 dispatch 向下调用新的 action或查看当前的state状态【要配合中间件才能起作用】

const lastAction = () =>{
    return {
        type: xxx
    }
}

const beforeAction = () => {
    return (dispatch,getState) => {
        console.log(getState())		// 获取state
        dispatch(lastAction())		// 手动dispatch调用下一个action
    }
}

实现原理:

applyMiddleware 利用 createStore 和 reducer 创建了一个 store,然后 store 的 getState 方法和 dispatch 方法又分别被直接和间接地赋值给 middlewareAPI 变量

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 利用传入的createStore和reducer和创建一个store
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
      )
    }
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 接着 compose 将 chain 中的所有匿名函数,组装成一个新的函数,即新的 dispatch
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

上面的compose方法可以接受一组函数参数,从右到左来组合多个函数,然后返回一个组合函数

compose(funcA, funcB, funcC)等价于compose(funcA(funcB(funcC())))

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

redux-thunk

redux-thunk 是一个处理异步事件的中间件,类似的还有 redux-promiseredux-saga 等中间件,使用方法和redux-thunk接近

npm install redux-thunk -s
原理
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 中间件的功能很简单,首先检查参数 action 的类型,如果是函数的话,就执行这个 action ,并把 dispatch, getState, extraArgument 作为参数传递进去,否则就调用 next 让下一个中间件继续处理 action

每个中间件最里层处理 action 参数的函数返回值都会影响 Store 上的 dispatch 函数的返回值,但每个中间件中这个函数返回值可能都不一样。比如上面这个 react-thunk 中间件,返回的可能是一个 action 函数,也有可能返回的是下一个中间件返回的结果。因此,dispatch 函数调用的返回结果通常是不可控的,最好不要依赖于 dispatch 函数的返回值

使用
// 直接在store文件里导入即可(同时要导入使用applyMiddleware方法)
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers'

export default createStore(
    rootReducer,
    applyMiddleware(thunk)
)    

使用中间件处理前:

使用中间件处理后:

将action传递给中间件,每一个中间件可以处理action并返回新的action,最后一个中间件处理完action后才会将最终的action传递给reducer处理,因此保证了reducer处理的是异步执行完步后的action,不会因为立即执行dispatch而没有及时拿到到action报错

即由于rudux的 action 仅支持原始对象,如果要执行副作用,则需要引入中间件,最终都是由一个 reducer 去处理一个最终 action,只是要拿到这个最终 action,如果是经过副作用处理过得,则需要用中间件处理一下

action处理

核心用法只有一个:reducer只针对最后的action作出响应,其他action是为了触发下一个action并将数据传递

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'

// reducer
const rootReducer = (initState=0, action) => {
    console.log('执行reducer')
    switch(action.type) {
		case('DEMO_TYPE'):
            console.log('执行DEMO_TYPE')
            // {type: "DEMO_TYPE", payload: {data: 2}}  说明数据已经进行处理并传递给最终的reducer
            console.log(action)    
			return initState + action.payload.data
		default: 
			return initState
	}
}

// store
let store =  createStore(
    rootReducer,
    applyMiddleware(thunk)
)    

// action
const action_A = (data) => {
    return(dispatch) => {
        data += 1
        dispatch(action_B(data))
    }
}

const action_B = (data) => {
    return(dispatch) => {
        data += 1
        dispatch(action_C(data))
    }
}

const action_C = (data) => {
    return {
        type: 'DEMO_TYPE',
        payload: {
            data: data
        }
    }
}


const event = () => {
    store.dispatch(action_A(0))     // 传递数据并启动action链
}

ReactDOM.render(
    <div>
        <button onClick={event}>触发dispatch</button>
    </div>,
  document.getElementById('root')
);
// 只打印一遍'执行reducer'和'执行DEMO_TYPE',说明多次dispatch只有action_C触发了reducer

异步的中间件处理

// reducer
const reducer = (initState = 0, action) => {
	switch(action.type) {
		case('TYPE_DEMO'):
			console.log('执行reducer')
			return initState + 1
		default: 
			return initState
	}
}

let store = createStore(
  reducer,
  applyMiddleware(thunk)
)    

const action_1 = () => {
  console.log('执行中间件1')
  return {
      type: 'TYPE_DEMO'
  }
}

const action_2 = () => {
  return (dispatch) => {
      setTimeout(() => {
          console.log('执行中间件2')
          dispatch(action_1())
      },2000) 
  }
}

const action_3 = () => {
  return (dispatch) => {
          console.log('执行中间件3')
          dispatch(action_2())
      }
}

export const action_4 = () => {
  return (dispatch) => {
      setTimeout(() => {
          console.log('执行中间件4')
          dispatch(action_3())
      },2000) 
  }
}

const dispatchEvent = () => {
	store.dispatch(action_4())
}

ReactDOM.render(
	<>
		<button onClick={dispatchEvent}>触发dispatch按钮</button>
	</>,
	document.getElementById('root')
);


// 点击按钮执行结果:执行中间件4 → 执行中间件3 → 执行中间件2 → 执行中间件1 → 执行reducer

使用redux-thunk在真实开发中实现数据请求的写法:

因为thunk可以在函数中拿到dispatch方法,而且有async函数可以阻塞,因此实际上不需要设置多个action层层嵌套,只需要在一个回调方法中统一处理数据最后传递给reducer就行

const action = (data) => {
    return {
        type: 'TYPE_DEMO',
        payload: {
            data
        }
    }
}

asyncAction = async () => {
    return async (dispatch) => {
		let data = await fetch(url, params)		// 将请求到的数据交给下一个action处理
        dispatch(action(data))					// 这里就用到了中间件的功能
    }
}

dispatch(asyncAction())		// 等于触发了asyncAction后将拿到的data数据付给acion再最终dispatch这个action触发reducer

// 最终执行reducer传入的是 {type: 'TYPE_DEMO',payload: {data} 对象
const reducer = (state = xxx, action) => {
    switch(action.type) {
    	case: 'TYPE_DEMO':
            console.log('对数据进行处理并返回新的state')
    }
}

可以在请求前后另外dispatch一些操作

比如在请求开始前将一个表示加载中的state设置成true,在请求完成时设置成flase,就实现需要在请求加载中时做的一些操作的效果

getData = async () => {
    return async(dispatch, getState) => {
        try {
            dispatch({type: 'GET_WEATHER_START'})
            const result = await fetch(url, params)
            dispatch(action(data))
            dispatch({type: 'GET_WEATHER_END'})
    	} catch(error) {
        	dispatch({type: 'GET_WEATHER_ERROR', error: err})
    	}     
	} 
}

redux-saga

redux-saga比redux-thunk更加强大,不止可以充当中间件的作用,可以实现多种代码效果

npm install --save redux-saga

用作中间件总体流程和thunk一样,只是处理中间action的方式变成了用saga而非thunk而已


基础配置

写上一个saga函数

// src/saga.js
export function* helloSaga() {
  console.log('Hello Sagas!');
}

运行 helloSaga 之前,必须使用 applyMiddleware 将 middleware 连接至 Store,然后使用 sagaMiddleware.run(helloSaga) 运行 Saga

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { helloSaga } from './sagas'
import reducer from 'xxxx'

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
  reducer,	// 这个reducer是用combineReducers综合导出的reducer
  applyMiddleware(sagaMiddleware)	// 声明使用saga中间件
)

sagaMiddleware.run(helloSaga)	// 需要指明要跑的根saga,这个saga一般会写在src下的saga.js文件

异步调用
  • Sagas 被实现为 Generator 函数,它会 yield 对象到 redux-saga middleware
  • 被 yield 的对象都是一类指令,指令可被 middleware 解释执行
  • 当 middleware 取得一个 yield 后的 Promise,middleware 会暂停 Saga,直到该 Promise 完成
  • 一旦 Promise 被 resolve,middleware 会恢复 Saga 接着执行,直到遇到下一个 yield
import { put, delay } from 'redux-saga/effects'

export function* demoAsync() {
  yield delay(1000)		// 延时函数,单位ms
  yield put({ type: 'DEMO'})
}       
// demoAsync Saga 通过delay(1000)延迟了1秒钟,然后才dispatch一个action
// 相当于await,执行完deley(1000)才会开始执行put({type:'DEMO'})
// Generator函数输出结果
const gen = demoAsync()
gen.next() // => { done: false, value: <result of calling delay(1000)> }
gen.next() // => { done: false, value: <result of calling put({type: 'DEMO'})> }	
gen.next() // => { done: true, value: undefined }

添加一个saga,使用 takeEvery 监听所有的 DEMO_ASYNC action,并在 action 被匹配时执行 demoAsync 任务

import { delay } from 'redux-saga'
import { put, takeEvery } from 'redux-saga/effects'

export function* demoAsync() {
  yield delay(1000)
  yield put({ type: 'DEMO' })
}

export function* watchDemoAsync() {
  yield takeEvery('DEMO_ASYNC', demoAsync)
}

同时启用多个 saga,一般将配置同时启动多个sage的方法命名为 rootSaga,作为所有saga的启动器,且 sagaMiddleware.run(rootSaga)

import { delay } from 'redux-saga' 
import { put, takeEvery, all } from 'redux-saga/effects'
import { helloSaga } from './sagas'

export function* demoAsync() {
  yield delay(1000)
  yield put({ type: 'DEMO' })
}

export function* watchDemoAsync() {
  yield takeEvery('DEMO_ASYNC', demoAsync)
}

// all 方法 yield 了一个数组,值是调用 helloSaga 和 watchIncrementAsync 两个 Saga 的结果
export default function* rootSaga() {
  yield all([
    helloSaga(),
    watchIncrementAsync()
  ])
}

总体使用思想

put(action) = dispatch(action),reducer接收到对应的action,type 就会对数据进行相应操作,同时使用辅助函数监听对应的action.type并调用对应saga函数,最后通过react-reduxconnect回到视图中,完成一次数据驱动视图(MVVM)

可以使用其他effect方法,帮助拿到对应请求数据,再执行put()将对应操作所需数据传入(如call)

实际思想和thunk一样,将数据处理完必后将最终的action传递给put函数触发reducer从而操作state


创建Effects
使用 redux-saga/effects 包里提供的函数来创建 Effect
【资料参考https://juejin.im/post/5ad83a70f265da503825b2b4】

redux-saga ,Sagas 都用 Generator 函数实现,从 Generator 里 yield 纯 JavaScript 对象表达 Saga 逻辑, 这个对象就是 Effect,因为Effect的存在,方便saga测试异步操作

Effect本质是一个特定的函数,返回的是纯文本对象(解释执行的信息),通过Effect函数,会返回一个字符串,saga-middleware根据这个字符串来执行真正的异步操作,可以具体表现成如下形式:

异步操作——>Effect函数——>纯文本对象——>saga-middleware——>执行异步操作

因此effect的api方法在使用时要加上yield

function * rootSaga() {
	const resultA = yield put({type: 'TEST'})
	const resultB = put({type: 'TEST'})
	console.log(resultA)	// 触发传入的action
	console.log(resultB)	// 一个包含effect内容的对象,用于给saga解析
}

Effect 一般发送给 middleware 的指令以执行的操作:

  • 发起一个异步调用(如发一起一个 Ajax 请求)
  • 发起其他的 action 从而更新 Store
  • 调用其他的 Sagas
yield takeEvery('DEMO_ASYNC', demoAsync)	// 如takeEvery('DEMO_ASYNC', demoAsync) 返回一个对象,称为effect

Effect创建器

下面介绍的api都是effect创建函数

  • 每个函数都会返回一个普通 Javascript 对象,并且不会执行任何其它操作
  • 执行是由 middleware 在上述迭代过程中进行的
  • middleware 会检查每个 Effect 的描述信息,并进行相应的操作

pattern 类型

大部分的 Effect 创建函数的参数都为 pattern 类型,它有如下规则:

  • 如果以空参数或 '*' ,那么将匹配所有发起的 action

  • 如果是一个函数,那么将匹配 pattern(action) 为 true 的 action

  • 如果是一个字符串,那么将匹配 action.type === pattern 的 action‘

  • 如果它是一个数组,那么数组中的每一项都适用于上述规则 —— 因此它是支持字符串与函数混用的

// 将匹配所有 action
take() 	
take('*')

// 将匹配 entities 字段为真的 action
take(action => action.entities)		
// 如果 pattern 函数上定义了 toString,action.type 将改用 pattern.toString 来测试
// 这个设定在使用 action 创建函数库(如 redux-act 或 redux-actions)时非常有用

// 匹配type === 'INCREMENT_ASYNC' 的action
take('INCREMENT_ASYNC')	

// 一般都是用纯字符串数组
take(['INCREMENT', 'DECREMENT']) 	// 将匹配 INCREMENT 或 DECREMENT 类型的 action
middleware 提供了一个特殊的 action —— END
	如果发起 END action,则无论哪种 pattern,只要是被 take Effect 阻塞的 Sage 都会被终止
	假如被终止的 Saga 下仍有分叉(forked)任务还在运行,那么它在终止任务前,会先等待其所有子任务完成

put

put(action):使用和功能跟 dispatch 一样,但是一般只在saga函数内部使用,如果在外部想触发reducer还是用 dispatch,可以理解为和thunk中将 dispatch 方法传入返回函数中手动调用一样

// 直接使用 action 对象
put({type:'DEMO_TYPE'})		

// 使用 action 创建函数
actionFun = (params) => {
    return {
        type: prarms
    }
}
put(actionFun('DEMO_TYPE'))

通过put可以实现和react thunk一样的功能(put中只能传进一个对象,如果像thunk那样传递一个嵌套的调用函数会报错)

const fun = () => {
	return new Promise((resove) => {
		setTimeout(() => {
			console.log('阻塞执行')
			resove()				// call在promise中遇到resolve前会堵塞
		},1000)
	})
}

function* Demo() {	
	yield call(fun)					// 阻塞操作
	yield put({type: 'ADD'})		// 等待call执行完毕后才dispatch一个最终action给reducer处理,这一步体现了saga的中间件思想
}

take

take(pattern):参数是pattern类型,规则如上,它的作用主要体现在触发action后

take是阻塞的,主要用来监听 action.type,只有dispatchput 对应的 action.type ,才会继续往下执行程序,否则会一直堵塞

创建一个 Effect 描述信息,用来命令 middleware 在 Store 上等待指定的 action,Generator 将暂停直到在发起与 pattern 匹配的 action 之前,即一般用take来等待用户的输入操作,如请求到信息后,再点击一次按钮,信息才显示

take 和 takeEvery 都是监听某个 action, 但是两者的作用却不一致,takeEvery 是每次 action 触发的时候都响应,而 take 则是执行流执行到 take 语句时才响应
takeEvery 只是监听 action, 并执行相对应的处理函数,对何时执行 action 以及如何响应 action 并没有多大的控制权,被调用的任务无法控制何时被调用,并且它们也无法控制何时停止监听,它只能在每次 action 被匹配时一遍又一遍地被调用
take 可以在 generator 函数中决定何时响应一个 action 以及 响应后的后续操作
import React from 'react';
import ReactDOM from 'react-dom';
import createSagaMiddleware from 'redux-saga'
import { createStore, applyMiddleware, Action } from 'redux'
import { takeEvery, take, all } from 'redux-saga/effects'

// reducer
const reducer = (initState:number = 0, action: Action) => {
	switch(action.type) {
		case('ADD'):
			console.log('执行reducer')
			return initState + 1
		default: 
			return initState
	}
}

// saga
const sagaMiddleware = createSagaMiddleware()

const store = createStore(
	reducer,	
	applyMiddleware(sagaMiddleware)	
)

function* A() {	
	yield take('ADD')
	console.log('执行take')
	
}

function* B() {
	yield takeEvery('ADD', () => {
		console.log('takeEvery')
	})
}

function* C() {
	yield take('DEC')
    console.log('执行DEC')
}

function* D() {
    console.log('开始执行')
}

function* rootSaga() {
	yield all([A(), B(), C(), D()])}

sagaMiddleware.run(rootSaga)

// 触发dispatch事件
const dispatchEvent = () => {
	store.dispatch({type: 'ADD'})
}

ReactDOM.render(
	<>
		<button onClick={dispatchEvent}>触发dispatch按钮</button>
	</>,
	document.getElementById('root')
);

// 页面刷新,打印:开始执行,sagaMiddleware.run(rootSaga) 是启动所有saga的起点
// 第一次点击按钮,打印:执行reducer → 执行take → 执行takeEvery,说明先触发的reducer才触发saga函数
// 多次点击按钮,打印:reducer → 执行takeEvery,说明take的执行次数由take语句个数决定,takeEvery每次都会触发
// 不打印 `执行DEC`,说明不对应的action.type会被take阻塞,不会往下执行

使用 take 来模拟 takeEvery

可以在take的返回值中拿到传入的完整action对象,而takeEvery可以在回调函数的形参中取得

import { takeEvery, take } from 'redux-saga'

function* watchTakeEvery() {
    // 使用回调函数去执行触发操作
    yield* takeEvery('*', function* logger(action) {		
        console.log(action)	
    })
}

function* watchTake() {
    // 不需要回调函数,与take并行
    while(true) {
        const action = yield take('*')
        console.log(action)
    })
}

//  while(true)的作用:一旦到达流程最后一步(logger),通过等待一个新的任意的 action 来启动一个新的迭代(logger 流程)

take阻塞的其他用法:

  • 触发次数执行
// 在 take 初次的 3 个 TODO_CREATED action 之后, watchFirstThreeTodosCreation Saga 将会使应用显示一条祝贺信息然后中止,这意味着 Generator 会被回收并且相应的监听不会再发生

import { take, put } from 'redux-saga/effects'

function* watchFirstThreeTodosCreation() {
    for (let i = 0; i < 3; i++) {
        const action = yield take('TODO_CREATED')
        }
    yield put({type: 'SHOW_CONGRATULATION'})
}
// 一共有三条 yield take('TODO_CREATED') 语句,因此直到put('TODO_CREATED')三次前,put({type: 'SHOW_CONGRATULATION'})一直处于不执行状态,当执行完三次put('TODO_CREATED')后执行该语句
  • 阻塞状态

    由于take具有堵塞作用,如果写多个take则有在不同状态执行不同的效果,如登入和注销

function* loginFlow() {
  while (true) {
    yield take('LOGIN')
    // 一系列登入后的操作
    yield take('LOGOUT')
    // 一系列注销后的操作
  }
}

call 和 fork

这两个函数主要都是用来发起异步操作的(如发送请求),不同的是 call 发起的是阻塞操作,即必须等待该异步操作完全完成才能进行后续的操作,而 fork 是非阻塞的,因此可以使用 fork 来实现多个并发的请求操作(fork相当于生成了一个task——一个在后台运行的进程)

  • call(fn, ...args):第一个参数是一个generator函数(也可以是一个返回Promise或任意其它值的普通函数),后面的参数都是依次传入这个函数的形参数值,返回值是promise的resolve结果

    call默认是接收到promise成功标志时解除阻塞,但是如果是普通函数的话,可能达不到阻塞的效果

const fun = () => {
	return new Promise((resove) => {
		setTimeout(() => {
			console.log('阻塞执行')
			resove('promise结果')				// call在promise中遇到resolve前会堵塞
		},1000)
	})
}

function * rootSaga() {
	const result = yield call(fun)			// result内容为:'promise结果'
	console.log('执行完毕')
}
// 执行结果:阻塞执行 → 执行完毕
const fun = () => {
	setTimeout(() => {
		console.log('阻塞执行')
	}, 4000);
}

function * rootSaga() {
	yield call(fun)
	console.log('执行完毕')
}
// 执行结果:执行完毕 → 阻塞执行 

搭配take使用的例子:

function* loginFlow() {
  while(true) {
    const {user, password} = yield take('LOGIN_REQUEST')	// 拿到触发reducer的`LOGIN_REQUEST`分支的action并解构
    const token = yield call(authorize, user, password)		// 将用户信息传入登入函数authorize,拿到登入信息
    if(token) {
      yield call(Api.storeItem({token}))
      yield take('LOGOUT')
      yield call(Api.clearItem('token'))
    }
  }
}
  • fork(fn, ...args):参数同理,与call不同,返回的是一个任务标志task,可以和下面api搭配使用、
    • join(taks):阻塞等待直到该fork任务返回的结果
    • cancel(task):取消指定的任务,
    • cancelled():返回一个布尔值,用于判断所在generator函数是否被取消,通常在finally区块中使用该effect来运行取消时的专用代码
// join的效果
const fun = () => {
	return new Promise((resove) => {
		setTimeout(() => {
			console.log('执行完毕')
			resove(1)				
		},1000)
	})
}

function * rootSaga() {
	const task = yield fork(fun)
	let result =  yield join(task)
	console.log(result)
}
// 打印结果:执行完毕 → 1,说明join阻塞了下面打印语句的执行
// cancel的效果
function * rootSaga() {
	const taskA = yield fork(fun)
	const taskB = yield fork(fun)
	console.log(taskA.isRunning())		// true
	console.log(taskB.isRunning())		// false
}
// cancelled的效果
function* fun (name){
	try {
		yield new Promise((resove) => {
			setTimeout(() => {
				console.log('执行完毕')
				resove(1)				
			},10000)
	})
	} finally {
		if(yield cancelled()) {
			console.log(`${name}任务被取消了`)
		}
	}

}

function * rootSaga() {
	const taskA = yield fork(fun,'A')
	const taskB = yield fork(fun,'B')
	yield cancel(taskB)
	console.log(taskA.isRunning())		// true
	console.log(taskB.isRunning())		// false
}
// 打印:B该任务被取消了,说明取消B任务时会触发cancelled()

​ 通过 forkmiddleare.runrunSaga 运行Saga会返回一个任务标志task

方法返回值
task.isRunning()若任务还未返回或抛出了一个错误则为 true
task.isCancelled()若任务已被取消则为 true
task.result()任务的返回值。若任务仍在运行中则为 undefined
task.error()任务抛出的错误。若任务仍在执行中则为 undefined
task.done一个 Promise:以任务的返回值 resolve 或 以任务抛出的错误 reject
task.cancel()取消任务(如果任务仍在执行中)
const fun = () => {
	return new Promise((resove) => {
		setTimeout(() => {
			resove(100)				
		},1000)
	})
}

function * rootSaga() {
	const task = yield fork(fun)

	// 任务执行中
	console.log(task.isRunning())		// true	
	console.log(task.result())			// undefined	从这里可以看出和join的区别,join是等待完成才拿结果
	console.log(task.done)				// undefined
	
	// 任务结束
	setTimeout(() => {
		console.log(task.isRunning())	// false
		console.log(task.result())		// 100
		console.log(task.done)			// undefined	具体为什么待查清
	},2000)
}
const fun = () => {
	return new Promise((resove) => {
		setTimeout(() => {
			console.log('执行完毕')
			resove(1)				
		},10000)
	})
}

function * rootSaga() {
	const taskA = yield fork(fun)
	const taskB = yield fork(fun)
	console.log(taskA.isRunning())			// true
	console.log(taskB.isRunning())			// true
	taskB.cancel()							
	setTimeout(() => {
		console.log(taskA.isRunning())		// ture
		console.log(taskB.isRunning())		// flase
	},2000)
}
// task.cancel()和cancel(task)不同的是由于不是effect函数,因此不用yield才能生效,如果这里直接使用cancel(taskB),那下面打印的会是true

上面代码有一个问题:即使在运行过程中取消了taskB继续执行,但是10s后仍会打印两次执行完毕,即有取消了任务操作,但是并没有马上中断后续任务的执行

// 改成Generator函数同理
function* fun() {
	yield new Promise((resove) => {
		setTimeout(() => {
			console.log('执行完毕')
			resove(1)				
		},10000)
	})
}

改写fun函数,使用请求的方式,由效果可知,若要令取消任务生效,需要设置为 Generator 函数(且无法使用延时函数进行模拟,中断一次请求和取消一次延时任务有点差异,这个待验证)

// 打印了两次请求数据data
const fun = async () => {
    let data = await fetch('www.baidu.com')	
    console.log(data)
}
// 打印了一次请求数据data
function* fun(){
    let data = yield fetch('www.baidu.com')
	console.log(data)
}

辅助函数
  • takeEvery(pattern, saga, ...args)

takeEvery 允许多个 saga 任务同时启动,尽管之前还有一个或多个 实例尚未结束,我们还是可以启动一个新的任务

如:点击一个按钮发送一个异步请求,当请求还没完全结束时,再次点击按钮,则再发送一次全新的请求

import { takeEvery } from 'redux-saga/effects'

function* getDataSaga() {
  try {
      yield put({type: 'OPEN_LOADING'})			// 开启加载 
      const res = yield call(() => fetch('www.baidu.com'))	// 发送请求
      if(res) {
          yield put({type: 'SUCCESS'})			// 成功请求
      }
  } catch(err) {
      yield put({type: 'ERROR'})				// 错误处理
  } finally {
      yield put('CLOSE_LOADING')				// 在结束时关闭加载
  }
}  
            
takeEvery('GET_DATA_REQUEST', getDataSaga)		// 监听 action.type为GET_DATA_REQUEST的action,并运行getDataSaga()
  • takeLatest(pattern, saga, ...args)

    takeLatest 只允许一个 demoAsync 任务在执行,并且这个任务是最后被启动的那个,如果已经有一个任务在执行的时候启动另一个 demoAsync ,那之前的这个任务会被自动取消

    如:点击一个按钮发送一个异步请求,当请求还没完全结束时,再次点击按钮,取消未完成的请求,再发送一次全新的请求

import { takeLatest } from 'redux-saga/effects'
...
takeLatest('DEMO_ASYNC', demoAsync)

当触发辅助函数时,会把触发的action对象传入对应的saga函数

dispatch({type: 'DEMO', payload: '数据'})

function * sagaDemo (action) {
    console.log(action)		// {type: 'DEMO', payload: '数据'}
}

takeEvery('DEMO', sagaDemo)	

组合器

用于组合各Effects

  • all([...effects]):与Promise中的all类似,middleware 并行地运行多个 Effect,并等待它们全部完成,返回一个数组,用于接收执行结果
import { call, race } from `redux-saga/effects`

function* mySaga() {
  const [customers, products] = yield all([
    call(Demo_A),
    call(Demo_B)
  ])
}

也可以直接省略all,因为当 yield 后面是一个数组时,数组里面的操作将按照 Promise.all 的执行规则来执行,genertor 会阻塞所有的 effects 被直到所有 effects 执行完成

// 同步执行和按顺序的写法区别
import { call } from 'redux-saga/effects'

function * fetch(payload) {
    return fetch(
        `http://www.baidu.com/payload=${payload}`,
        { method: 'GET' }
    ).then(res => res.json())
}

//同步执行
const [users, products] = yield [
  call(fetch, '/users'),
  call(fetch, '/products')
]

//顺序执行
const users = yield call(fetch, '/users'),
const products = yield call(fetch, '/products')
当并发运行 Effect 时,middleware 将暂停 Generator,直到以下任一情况发生:
	- 所有Effect都成功完成:返回一个包含所有Effect结果的数组,并恢复Generator
	- 在所有Effect完成之前,有一个Effect被reject:在Generator中抛出reject错误
  • race([...effects]):与Promise中的race类似,如果任意一个effect先完成(无论reject或resolve),将其他effect取消执行,然后继续执行后续代码
// 发起call(fetchUsers)操作,但是如果在这个操作还没有完成之前,Store上先发起了一个CANCEL_FETCH类型的 action,则取消call(fetchUsers)

import { take, call, race } from `redux-saga/effects`
import fetchUsers from './path/to/fetchUsers'

function* fetchUsersSaga {
  const [ response, cancel ] = yield race([
      call(fetchUsers),
      take('CANCEL_FETCH')
  ])
}

代码设计思路

如实现一个登入的请求并保存返回的登入状态信息总体的思路:

  1. 外部触发 dispatch 函数,将从表单中获取到的登入的信息传给能触发saga监听函数的action参数中
  2. action拿到这些信息,通过saga监听函数takeEvery 触发saga函数
  3. 通过saga去请求后端并拿到返回数据,同时触发影响reducer的action
  4. reducer拿到数据信息,保存在redux的state中,用于在组件中使用
总体流程:
	dispatch → 触发acion → 触发监听函数 → 触发saga函数 → 触发reducer → 更新state
	实际上就是相当于 dispatch(action) → 中间件对action进行处理 → dispatch(最终action) → 触发reducer → 更新state

上面需要值得注意的是,有两种action,一种是为了触发监听函数从而启动saga,一种是为了触发reducer函数,从这里可以看出saga是如何作为中间件来使用的

// action
const action = (info) => ({
    type: 'SAGA_ACTION',
    payload: info
})

// 外部触发action
dispatch(action(info))		// 将信息传给action

// 监听函数
function* rootSaga() {
    yield takeEvery('SAGA_ACTION',saga)		// 监听到有'SAGA_ACTION'的action被调用,触发saga函数	
}

// saga
function* saga(action) {
    const { payload } = action		// 拿到触发takeEvery函数的action对象并取出info数据
    yield call(fetch(...))		// 将拿到的数据发送给后端操作
    // 可能对返回数据做出一些处理,然后dispatch一个带有最终数据的action触发reducer函数
    yield put({
        type: 'REDUCER_ACTION',
        payload: newData
    })
}

// reducer
const reducer = (initState, action) => {
    switch(action.type) {
        // 从saga那dispatch的'REDUCER_ACION'被reducer判断并执行
        case 'REDUCER_ACION':
            const { payload } = action	// 拿到最终数据
            // 进行一些操作,最终更新state
            return newState
    }
}

超时自动取消

利用saga的 race 实现超时自动取消功能(axios也能做到但是这里使用saga来实现)

// 整个功能目的:发出一个请求,可以手动取消或者自动超时取消,如果一段时间内没有手动取消任务,则会执行自动超时取消(假设请求的时间很长,超过超时自动取消的时间)
function* controlSaga (action) {
    const task = yield fork(getDataSaga); // 运行getDataSaga函数
    yield race([ // 利用rece谁先来用谁的原则,完美解决 超时自动取消与手动取消的 的问题
        take('CANCEL_REQUEST'), // 手动取消action
        call(delay, 1000) // 延时1秒
    ]);
    yield cancel(task);
}
// call和take任意一个完成都会取消阻塞往下执行cancel,因此可以理解为cancel最多被阻塞

高阶函数封装

可以写一个高阶函数(返回一个Generator函数)统一封装saga的功能

// controlSaga.js
import { take, fork, race, call, cancel, put } from 'redux-saga/effects';
import { delay } from 'redux-saga';

function controlSaga (fn) {
    /**
     * @param timeOut: 超时时间, 单位 ms, 默认 5000ms
     * @param cancelType: 取消任务的action.type
     * @param showInfo: 打印信息 默认不打印
     */
    return function* (...args) {
        // 这边思考了一下,还是单单传action过去吧,不想传args这个数组过去, 感觉没什么意义
        const task = yield fork(fn, args[args.length - 1]);
        const timeOut = args[0].timeOut || 5000; // 默认5秒
        // 如果真的使用这个controlSaga函数的话,一般都会传取消的type过来, 假如真的不传的话,配合Match.random()也能避免误伤
        const cancelType = args[0].cancelType || `NOT_CANCEL${Math.random()}`;
        const showInfo = args[0].showInfo; // 没什么用,打印信息而已
        const result = yield race({
            timeOut: call(delay, timeOut),
            // 实际业务需求
            handleToCancel: take(cancelType)
        });
        if (showInfo) {
            if (result.timeOut) yield put({type: `超过规定时间${timeOut}ms后自动取消`})
            if (result.handleToCancel) yield put({type: `手动取消,action.type为${cancelType}`})
        }
    
        yield cancel(task);
    }
}

export default controlSaga;
import controlSaga from './controlSaga';

function* demoSaga() {
	... 
}
    
function* rootSaga() {
    yield takeLatest('TYPE', controlZaga(demoSaga))
}

thunk和saga

假如有一个场景:用户在登录的时候需要验证用户的 username 和 password 是否符合要求,双方的action写法会有很大不同

redux-thunk

// 使用thunk时,一般会把这两个action分成两个文件来写,这里为了方便不进行拆分
import request from 'axios';

// 获取用户数据的逻辑的action
export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}


// 验证登录的逻辑的action
export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));		// 在输入登入数据后验证用户信息
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

redux-saga

import request from 'axios';

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(userRequest, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST) 	// 等待Store上指定的action: LOGIN_REQUEST
    try {
      let { data } = yield call(loginRequest, { user, pass }); 	 // 阻塞,请求后台数据
      yield fork(loadUserData, data.uid); 	// 非阻塞执行loadUserData
      yield put({ type: LOGIN_SUCCESS, data }); 	//发起一个action,类似于dispatch
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

查看中间件状态

redux-logger

redux-logger会在console打印当前触发的action,包括action的上一个状态,当前状态,下一个状态

npm install redux-logger -s

logger 中间件必须放在所有中间件的最后,不然会出现部分action无法打印日志的情况

import logger from 'redux-logger'
import thunk from 'redux-thunk'

const store = createStore(
    reducer,
    applyMiddleware(thunk, logger)
)

一般会进行额外配置

import createLogger from 'redux-logger'
// 调用日志打印方法 collapsed是让action折叠,看着舒服点
const loggerMiddleware = createLogger({collapsed:true})

const store = createStore(
    reducer,
    applyMiddleware(loggerMiddleware)
)

redux-devtools

用于查看redux中间件的数据传递过程,使用不仅要在浏览器中安装插件(Redux Developer),也要在代码中配置,可以用于替代 redux-logger

chrom浏览器安装完成后右上角会出现图标,在配置的页面中可以点卡使用

npm install redux-devtools-extension -s
import { composeWithDevTools } from 'redux-devtools-extension';

const store = createStore(
    reducer,
    composeWithDevTools(applyMiddleware(中间件))
)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值