redux-saga 初识和使用
作者:grecep 发布时间:2023-07-15 16:20:17
redux-saga 是一个管理 Redux 应用异步操作的中间件,功能类似redux-thunk + async/await, 它通过创建 Sagas 将所有的异步操作逻辑存放在一个地方进行集中处理。
redux-saga 的 effects
redux-saga中的 Effects 是一个纯文本 JavaScript 对象,包含一些将被 saga middleware 执行的指令。这些指令所执行的操作包括如下三种:
发起一个异步调用(如发一起一个 Ajax 请求)
发起其他的 action 从而更新 Store
调用其他的 Sagas
Effects 中包含的指令有很多,具体可以异步API 参考进行查阅
redux-saga 的特点
方便测试,例如:
assert.deepEqual(iterator.next().value, call(Api.fetch, '/products'))
action 可以保持其纯净性,异步操作集中在 saga 中进行处理
watch/worker(监听->执行) 的工作形式
被实现为 generator
对含有复杂异步逻辑的应用场景支持良好
更细粒度地实现异步逻辑,从而使流程更加清晰明了,遇到 bug 易于追踪和解决。
以同步的方式书写异步逻辑,更符合人的思维逻辑
从 redux-thunk 到 redux-saga
假如现在有一个场景:用户在登录的时候需要验证用户的 username 和 password 是否符合要求。
使用 redux-thunk 实现
获取用户数据的逻辑(user.js):
// user.js
import request from 'axios';
// define constants
// define initial state
// export default reducer
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 });
}
}
验证登录的逻辑(login.js):
import request from 'axios';
import { loadUserData } from './user';
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
异步逻辑可以全部写进 saga.js 中:
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 });
}
}
}
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 });
}
}
难点解读
对于 redux-saga, 还是有很多比较难以理解和晦涩的地方,下面笔者针对自己觉得比较容易混淆的概念进行整理:
take 的使用
take 和 takeEvery 都是监听某个 action, 但是两者的作用却不一致,takeEvery 是每次 action 触发的时候都响应,而 take 则是执行流执行到 take 语句时才响应。takeEvery 只是监听 action, 并执行相对应的处理函数,对何时执行 action 以及如何响应 action 并没有多大的控制权,被调用的任务无法控制何时被调用,并且它们也无法控制何时停止监听,它只能在每次 action 被匹配时一遍又一遍地被调用。但是 take 可以在 generator 函数中决定何时响应一个 action 以及 响应后的后续操作。
例如在监听所有类型的 action 触发时进行 logger 操作,使用 takeEvery 实现如下:
import { takeEvery } from 'redux-saga'
function* watchAndLog(getState) {
yield* takeEvery('*', function* logger(action) {
//do some logger operation //在回调函数体内
})
}
使用 take 实现如下:
import { take } from 'redux-saga/effects'
function* watchAndLog(getState) {
while(true) {
const action = yield take('*')
//do some logger operation //与 take 并行
})
}
其中 while(true) 的意思是一旦到达流程最后一步(logger),通过等待一个新的任意的 action 来启动一个新的迭代(logger 流程)。
阻塞和非阻塞
call 操作是用来发起异步操作的,对于 generator 来说,call 是阻塞的操作,它在 Generator 调用结束之前不能执行或处理任何其他事情。,但是 fork 却是非阻塞操作,当 fork 调动任务时,该任务会在后台执行,此时的执行流可以继续往后面执行而不用等待结果返回。
例如如下的登录场景:
function* loginFlow() {
while(true) {
const {user, password} = yield take('LOGIN_REQUEST')
const token = yield call(authorize, user, password)
if(token) {
yield call(Api.storeItem({token}))
yield take('LOGOUT')
yield call(Api.clearItem('token'))
}
}
}
若在 call 在去请求 authorize 时,结果未返回,但是此时用户又触发了 LOGOUT 的 action,此时的 LOGOUT 将会被忽略而不被处理,因为 loginFlow 在 authorize 中被堵塞了,没有执行到 take('LOGOUT')那里
同时执行多个任务
如若遇到某个场景需要同一时间执行多个任务,比如 请求 users 数据 和 products 数据, 应该使用如下的方式:
import { call } from 'redux-saga/effects'
//同步执行
const [users, products] = yield [
call(fetch, '/users'),
call(fetch, '/products')
]
//而不是
//顺序执行
const users = yield call(fetch, '/users'),
products = yield call(fetch, '/products')
当 yield 后面是一个数组时,那么数组里面的操作将按照 Promise.all 的执行规则来执行,genertor 会阻塞知道所有的 effects 被执行完成
源码解读
在每一个使用 redux-saga 的项目中,主文件中都会有如下一段将 sagas 中间件加入到 Store 的逻辑:
const sagaMiddleware = createSagaMiddleware({sagaMonitor})
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)
其中 createSagaMiddleware 是 redux-saga 核心源码文件 src/middleware.js 中导出的方法:
export default function sagaMiddlewareFactory({ context = {}, ...options } = {}) {
...
function sagaMiddleware({ getState, dispatch }) {
const channel = stdChannel()
channel.put = (options.emitter || identity)(channel.put)
sagaMiddleware.run = runSaga.bind(null, {
context,
channel,
dispatch,
getState,
sagaMonitor,
logger,
onError,
effectMiddlewares,
})
return next => action => {
if (sagaMonitor && sagaMonitor.actionDispatched) {
sagaMonitor.actionDispatched(action)
}
const result = next(action) // hit reducers
channel.put(action)
return result
}
}
...
}
这段逻辑主要是执行了 sagaMiddleware(),该函数里面将 runSaga 赋值给 sagaMiddleware.run 并执行,最后返回 middleware。 接着看 runSaga() 的逻辑:
export function runSaga(options, saga, ...args) {
...
const task = proc(
iterator,
channel,
wrapSagaDispatch(dispatch),
getState,
context,
{ sagaMonitor, logger, onError, middleware },
effectId,
saga.name,
)
if (sagaMonitor) {
sagaMonitor.effectResolved(effectId, task)
}
return task
}
这个函数里定义了返回了一个 task 对象,该 task 是由 proc 产生的,移步 proc.js:
export default function proc(
iterator,
stdChannel,
dispatch = noop,
getState = noop,
parentContext = {},
options = {},
parentEffectId = 0,
name = 'anonymous',
cont,
) {
...
const task = newTask(parentEffectId, name, iterator, cont)
const mainTask = { name, cancel: cancelMain, isRunning: true }
const taskQueue = forkQueue(name, mainTask, end)
...
next()
return task
function next(arg, isErr){
...
if (!result.done) {
digestEffect(result.value, parentEffectId, '', next)
}
...
}
}
其中 digestEffect 就执行了 effectTriggerd() 和 runEffect(),也就是执行 effect,其中 runEffect() 中定义了不同 effect 执行相对应的函数,每一个 effect 函数都在 proc.js 实现了。
除了一些核心方法之外,redux-saga 还提供了一系列的 helper 文件,这些文件的作用是返回一个类 iterator 的对象,便于后续的遍历和执行, 在此不具体分析。
来源:http://www.codedata.cn/hacknews/152047495245427871
猜你喜欢
- 背景:项目中有多个组件调用同一接口,为提高代码可维护性,需要封装公共方法直接return 接口调用的结果export function ge
- 在将自定义的网络权重加载到网络中时,报错:AttributeError: 'dict' object has no attr
- 一、List移除某个值remove以Python 3.x版本为主remove:列表值移除方法1、函数编号函数名说明1remove移除列表中匹
- 目录1. 从标准输入中读取2. 单独打开一个文件3. 批量打开多个文件4. 读取的同时备份文件5. 标准输出重定向替换6. 不得不介绍的方法
- 官方链接:https://cli.vuejs.org/zh/guide/installation.html1.安装Vue cli3 关于旧版
- 手残删除python补救新建文件夹,下载下面的依赖wget http://vault.centos.org/7.2.1511/o
- 简述mat参照了函数设计,plot表示绘图的作用,lib则表示一个集合。今年在开源社区的推动下,Matplotlib在科学计算领域得到了广泛
- 前言昨天上线后通过系统报警发现了一个bug,于是紧急进行了回滚操作,但是期间有用户下单,数据产生了影响,因此需要排查影响了哪些订单,并对数据
- 目录前言Tips - django版本区别路由匹配无名分组&有名分组无名分组有名分组小提示反向解析路由不涉及分组的反向解析有名分组&
- 一、seaborn概述Seaborn是在matplotlib的基础上进行了更高级的API封装,从而使得作图更加容易,在大多数情况下使用sea
- 介绍本文中探索三个流行的 Python 图像增强库。图像分类器通常在训练更多的图像时表现得更好。在图像分类模型中,一个常见的问题是,模型不能
- 今天用python 使用pyinstaller打包exe出现错误环境pyqt5 + python3.6 32位在导入pyqt5包之前加上如下
- 定义:在Django框架中,模板是可以帮助开发者快速生成呈现给用户页面的工具模板的设计方式实现了我们MVT中VT的解耦,VT有着N:M的关系
- 有时候要用Javascript输常用的字符,比如每个页面都要有的脚注。这里提供一个转换脚本:将HTML自动转为JS代码<script&
- argparse 是python自带的命令行参数解析包,可以用来方便地读取命令行参数。一、传入一个参数import argpars
- Demo里的三种方法:方法1是两层div,兼容FF3.1a+, Safari 3+, Chrome, IE6/7方法2是两层div,除了IE
- mysql drop database命令用于删除一个数据库,如果试图使用drop database命令删除一个不存在的数据库,那么那么你会
- 介绍反射是元数据编程的一种形式,指的是程序获得本身结构的一种能力。不同语言的反射模型实现不一样,本文中的反射,仅仅指的是Go语言中的反射模型
- 函数使用def base64_to_image(base64_code): img_data = base64.b
- 1. lr_scheduler相关lr_scheduler = WarmupLinearSchedule(optimizer, warmup