redux-saga
中的effect
常用的几个是有区分出阻塞与非阻塞的,这里主要看下call
和fork
两者的区别。
实现效果
- 非阻塞的task执行,不用等到登录成功后请求的list接口完成,点击退出按钮可以立即退出
- 阻塞task的执行,必须等到登录成功后请求的list接口完成,点击退出按钮才有效。下面展示的点击效果,开始点击退出前几次是没有用的,没有反应。
以上两种方式都有各自使用场景,具体可以根据自己的需求去开发 - 第一种就是,登录后可以立即退出
- 第二种就是,登录后会请求列表接口,只有等接口请求完成了,点击退出才会有效。
代码逻辑
- 页面组件SagaLogin.js
import { Form, Input, Button } from "antd";
import { useState, useEffect } from "react";
import { LoginContainer } from "./style.js";
import { useSelector, useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import * as actionTypes from "../../store/sagas/actionTypes";
function SagaLogin() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const isLogin = useSelector((state) => state.getIn(["sys", "isLogin"]));
const navigator = useNavigate();
const dispatch = useDispatch();
const handleChangeName = (e) => {
setUsername(e.target.value);
dispatch({
type: actionTypes.CHANGE_USERNAME + "@@SAGA@@",
value: e.target.value,
});
};
const handleChangePwd = (e) => {
setPassword(e.target.value);
dispatch({
type: actionTypes.CHANGE_PASSWORD + "@@SAGA@@",
value: e.target.value,
});
};
const onFinish = (values) => {
console.log(values);
setIsLoading(true);
dispatch({ type: actionTypes.LOGIN + "@@SAGA@@", value: values });
};
useEffect(() => {
// 监听登录
if (isLogin) {
setIsLoading(false);
navigator("/saga-list", {
replace: true,
});
}
return () => {};
}, [isLogin]);
return (
<LoginContainer>
<div className="main">
<h2>Saga-Login</h2>
<br />
{isLogin}
<Form onFinish={onFinish}>
<Form.Item
label="用户名:"
name="username"
rules={[{ required: true, message: "Please input your username!" }]}
>
<Input
placeholder="请输入用户名"
value={username}
onChange={handleChangeName}
/>
</Form.Item>
<Form.Item
label="密码:"
name="password"
rules={[{ required: true, message: "Please input your password!" }]}
>
<Input
placeholder="请输入密码"
type="password"
value={password}
onChange={handleChangePwd}
/>
</Form.Item>
<Form.Item>
<Button block type="primary" htmlType="submit" loading={isLoading}>
登录
</Button>
</Form.Item>
</Form>
</div>
</LoginContainer>
);
}
export default SagaLogin;
- style.js
import styled from "styled-components";
import bg from "./image.png";
export const LoginContainer = styled.div`
height: 100vh;
background: url(${bg}) no-repeat center;
background-size: cover;
.main {
background: rgba(255, 255, 255, 0.5);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
h2 {
margin-bottom: 20px;
}
form {
width: 60%;
}
}
`;
- 跳转列表页面
import { useSelector, useDispatch } from "react-redux";
import { Button, Space, FloatButton } from "antd";
import * as actionTypes from "../../store/sagas/actionTypes";
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { AticleContainerContainer } from "./style";
function SagaList() {
const list = useSelector((state) => state.getIn(["sys", "articleList"]));
const userInfo = useSelector((state) => state.getIn(["sys", "userInfo"]));
const isLogin = useSelector((state) => state.getIn(["sys", "isLogin"]));
const navigator = useNavigate();
const dispatch = useDispatch();
const handleLoginOut = () => {
dispatch({
type: actionTypes.LOGIN_OUT + "@@SAGA@@",
});
};
useEffect(() => {
// 监听退出
if (!isLogin) {
navigator("/saga-login");
}
return () => {};
}, [isLogin]);
return (
<AticleContainerContainer>
<h1>Saga-List</h1>
<p>This is the SagaList page.</p>
<Space>
<p>{userInfo.get("username")}</p>
{userInfo.get("password")}
</Space>
<div>
<FloatButton
type="primary"
description={"退出"}
onClick={handleLoginOut}
>
退出登录
</FloatButton>
</div>
<ul>
{list.map((item) => {
return (
<li key={item.username}>
{item.username} --- {item.channelName}
</li>
);
})}
</ul>
</AticleContainerContainer>
);
}
export default SagaList;
- style.js
import styled from "styled-components";
export const AticleContainerContainer = styled.div`
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
ul {
list-style: none;
li {
margin: 10px 0;
}
}
`;
- store/index.js
import { createStore, applyMiddleware } from "redux";
import { combineReducers } from "redux-immutable";
import createSagaMiddleware from "redux-saga";
import defAllSags from "./sagas/index.js";
import CounterReducer from "./sagas/counterReducer";
import TaskOkReucer from "./sagas/taskOkReucer.js";
import sysLoginReducer from "./sagas/sysReucers.js";
const sagaMiddleware = createSagaMiddleware();
const reducers = combineReducers({
count: CounterReducer,
task: TaskOkReucer,
sys: sysLoginReducer, // 系统登录的reducer
});
const store = createStore(reducers, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(defAllSags);
export default store;
- store/sagas/sysReucers.js
import { fromJS } from "immutable";
import * as actionTypes from "./actionTypes";
const initState = fromJS({
isLogin: false,
articleList: [],
userInfo: {
username: "",
password: "",
},
});
function sysLoginReducer(state = initState, action) {
console.log("🚀 ~ sysLoginReducer ~ action:", action);
switch (action.type) {
case actionTypes.LOGIN_SUCCESS:
return state.set("isLogin", true);
case actionTypes.LOGIN_OUT:
return state.set("isLogin", false);
case actionTypes.CHANGE_USERNAME:
return state.mergeIn(["userInfo"], { username: action.value });
case actionTypes.CHANGE_PASSWORD:
return state.mergeIn(["userInfo"], { password: action.value });
case actionTypes.UPDATA_LIST:
return state.set("articleList", action.value);
default:
return state;
}
}
export default sysLoginReducer;
- store/sagas/sysSaga.js
import * as actionTypes from "./actionTypes";
import {
takeLatest,
fork,
all,
delay,
put,
call,
takeEvery,
} from "redux-saga/effects";
// 修改名称
function* handleChangeName(action) {
console.log("change name", action);
// yield delay(2000);
yield put({ type: actionTypes.CHANGE_USERNAME, value: action.value });
}
function* watchChangeName() {
yield takeLatest(actionTypes.CHANGE_USERNAME + "@@SAGA@@", handleChangeName);
}
function* handleChangePwd(action) {
console.log("change pwd", action);
// yield delay(2000);
yield put({ type: actionTypes.CHANGE_PASSWORD, value: action.value });
}
function* watchChangePwd(action) {
console.log("change pwd", action);
yield takeLatest(actionTypes.CHANGE_PASSWORD + "@@SAGA@@", handleChangePwd);
}
function* getList() {
try {
yield delay(3000);
const activityList = [
{ username: "username1", channelName: "channelName1" },
{ username: "username2", channelName: "channelName2" },
{ username: "username3", channelName: "channelName3" },
{ username: "username4", channelName: "channelName4" },
];
yield put({ type: actionTypes.UPDATA_LIST, value: activityList });
} catch (error) {
yield put({ type: "update_list_error", error });
}
}
function* handleLogout() {
console.log("logout");
yield put({ type: actionTypes.LOGIN_OUT });
}
function* handleLogin(action) {
yield delay(2000);
if (action.value.username === "admin" && action.value.password === "123456") {
yield put({ type: actionTypes.LOGIN_SUCCESS });
// 获取列表数据
yield call(getList);
} else {
yield put({ type: actionTypes.LOGIN_FAIL });
}
// 监听登出
yield takeEvery(actionTypes.LOGIN_OUT + "@@SAGA@@", handleLogout);
}
function* watchLogin(action) {
console.log("login", action);
yield takeLatest(actionTypes.LOGIN + "@@SAGA@@", handleLogin);
}
function* sysSaga() {
console.log("SYS SAGA");
// 监听所有的dispatch
yield all([fork(watchChangeName), fork(watchChangePwd), fork(watchLogin)]);
}
export default sysSaga;
在handleLogin
中,我们模拟判断账户密码,触发登录成功更改登录状态,在去请求getList
,这个是阻塞的。
这样一看没有什么问题,但是注意call
方法调用是会阻塞主线程的,具体来说:
- 在
call
方法调用结束之前,call
方法之后的语句是无法执行的- 如果
call(getList)
存在延迟,call(getList)
之后的语句yield takeEvery(actionTypes.LOGIN_OUT + "@@SAGA@@", handleLogout);
在call方法返回结果之前无法执行- 在延迟期间的登出操作会被忽略。
无阻塞调用
将yield call(getList)
改为 yield fork(getList)
,通过fork方法不会阻塞主线程,在getlist
接口未返回结果前点击登出,可以立刻响应登出功能,从而返回登陆页面。
- store/sagas/actionTypes.js
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
export const REQUEST_FAIL = "REQUEST_FAIL";
export const CHANGE_NAME = "CHANGE_NAME";
export const LOGIN_SUCCESS = "LOGIN_SUCCESS";
export const LOGIN = "LOGIN";
export const LOGIN_FAIL = "LOGIN_FAIL";
export const CHANGE_USERNAME = "CHANGE_USERNAME";
export const CHANGE_PASSWORD = "CHANGE_PASSWORD";
export const UPDATA_LIST = "UPDATA_LIST";
export const LOGIN_OUT = "LOGIN_OUT";
总结
通过前面的案例分析,我们可以概括出redux-saga
做为redux
中间件的全部优点:
- 统一
action
的形式,在redux-saga
中,从UI中dispatch
的action
为原始对象 - 集中处理异步等存在副作用的逻辑
- 通过转化
effects
函数,可以方便进行单元测试 - 完善和严谨的流程控制,可以较为清晰的控制复杂的逻辑。