现在有很多关于 redux 镇最新的孩子的讨论,redux-saga/redux-saga。它使用生成器函数来监听/调度动作。
在深入了解它之前,我想知道使用 redux-saga
而不是下面我将 redux-thunk
与 async/await 一起使用的方法的优缺点。
一个组件可能看起来像这样,像往常一样调度操作。
import { login } from 'redux/auth';
class LoginForm extends Component {
onClick(e) {
e.preventDefault();
const { user, pass } = this.refs;
this.props.dispatch(login(user.value, pass.value));
}
render() {
return (<div>
<input type="text" ref="user" />
<input type="password" ref="pass" />
<button onClick={::this.onClick}>Sign In</button>
</div>);
}
}
export default connect((state) => ({}))(LoginForm);
然后我的动作看起来像这样:
// auth.js
import request from 'axios';
import { loadUserData } from './user';
// define constants
// define initial state
// export default reducer
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 });
}
}
// more actions...
// 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 });
}
}
// more actions...
this.onClick
之前的 ::
是什么?
this
)的简写,又名 this.onClick = this.onClick.bind(this)
。通常建议在构造函数中使用较长的形式,因为在每次渲染时都会重新绑定简写形式。
bind()
将 this
传递给函数,但我现在开始使用 () => method()
。
在 redux-saga 中,与上面的示例等效的是
export function* loginSaga() {
while(true) {
const { user, pass } = yield take(LOGIN_REQUEST)
try {
let { data } = yield call(request.post, '/login', { user, pass });
yield fork(loadUserData, data.uid);
yield put({ type: LOGIN_SUCCESS, data });
} catch(error) {
yield put({ type: LOGIN_ERROR, error });
}
}
}
export function* loadUserData(uid) {
try {
yield put({ type: USERDATA_REQUEST });
let { data } = yield call(request.get, `/users/${uid}`);
yield put({ type: USERDATA_SUCCESS, data });
} catch(error) {
yield put({ type: USERDATA_ERROR, error });
}
}
首先要注意的是,我们使用 yield call(func, ...args)
形式调用 api 函数。 call
不执行效果,它只是创建一个像 {type: 'CALL', func, args}
这样的普通对象。执行被委托给 redux-saga 中间件,该中间件负责执行函数并使用其结果恢复生成器。
主要优点是您可以使用简单的相等检查在 Redux 之外测试生成器
const iterator = loginSaga()
assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))
// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
iterator.next(mockAction).value,
call(request.post, '/login', mockAction)
)
// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
iterator.throw(mockError).value,
put({ type: LOGIN_ERROR, error: mockError })
)
请注意,我们通过简单地将模拟数据注入迭代器的 next
方法来模拟 api 调用结果。模拟数据比模拟函数简单得多。
要注意的第二件事是对 yield take(ACTION)
的调用。动作创建者在每个新动作上调用 Thunks(例如 LOGIN_REQUEST
)。即动作不断推送到 thunk,而 thunk 无法控制何时停止处理这些动作。
在 redux-saga 中,生成器 pull 下一个动作。即他们可以控制何时监听某些动作,何时不监听。在上面的示例中,流指令放置在 while(true)
循环中,因此它将侦听每个传入的操作,这在某种程度上模仿了 thunk push 行为。
拉式方法允许实现复杂的控制流。例如,假设我们要添加以下要求
处理 LOGOUT 用户操作
在第一次成功登录时,服务器返回一个令牌,该令牌在存储在 expires_in 字段中的某个延迟后过期。我们必须在每个 expires_in 毫秒的后台刷新授权
考虑到在等待 api 调用的结果(初始登录或刷新)时,用户可能会在中间注销。
您将如何使用 thunk 实现它?同时还为整个流程提供完整的测试覆盖?以下是 Sagas 的外观:
function* authorize(credentials) {
const token = yield call(api.authorize, credentials)
yield put( login.success(token) )
return token
}
function* authAndRefreshTokenOnExpiry(name, password) {
let token = yield call(authorize, {name, password})
while(true) {
yield call(delay, token.expires_in)
token = yield call(authorize, {token})
}
}
function* watchAuth() {
while(true) {
try {
const {name, password} = yield take(LOGIN_REQUEST)
yield race([
take(LOGOUT),
call(authAndRefreshTokenOnExpiry, name, password)
])
// user logged out, next while iteration will wait for the
// next LOGIN_REQUEST action
} catch(error) {
yield put( login.error(error) )
}
}
}
在上面的示例中,我们使用 race
来表达我们的并发要求。如果 take(LOGOUT)
赢得比赛(即用户点击了注销按钮)。比赛会自动取消authAndRefreshTokenOnExpiry
后台任务。如果 authAndRefreshTokenOnExpiry
在 call(authorize, {token})
调用中间被阻止,它也将被取消。取消自动向下传播。
您可以找到 runnable demo of the above flow
除了库作者相当彻底的回答之外,我还将添加我在生产系统中使用 saga 的经验。
专业版(使用传奇):
可测试性。测试 sagas 非常容易,因为 call() 返回一个纯对象。测试 thunk 通常需要您在测试中包含一个 mockStore。
redux-saga 带有许多有用的关于任务的辅助函数。在我看来,saga 的概念是为您的应用程序创建某种后台工作程序/线程,它充当 react redux 架构中缺失的部分(actionCreators 和 reducers 必须是纯函数。)这导致了下一点。
Sagas 提供了独立的地方来处理所有的副作用。根据我的经验,修改和管理通常比 thunk 操作更容易。
缺点:
生成器语法。
很多概念要学。
API 稳定性。似乎 redux-saga 仍在添加功能(例如 Channels?)并且社区没有那么大。如果库有一天会进行非向后兼容的更新,则存在问题。
API stability
的 Con 作为更新以反映当前情况。
我想根据我的个人经验添加一些评论(同时使用 sagas 和 thunk):
Sagas 非常适合测试:
您不需要模拟带有效果的函数
因此测试干净、易读且易于编写
使用 sagas 时,动作创建者大多返回纯对象字面量。与 thunk 的 promise 不同,它也更容易测试和断言。
萨迦更强大。你可以在一个 thunk 的动作创建器中做的所有事情,你也可以在一个 saga 中做,但反之亦然(或者至少不容易)。例如:
等待一个动作/动作被分派(采取)
取消现有的例程(cancel、takeLatest、race)
多个例程可以监听同一个动作(take, takeEvery, ...)
Sagas 还提供了其他有用的功能,这些功能概括了一些常见的应用程序模式:
监听外部事件源的通道(例如 websockets)
叉子模型(叉子,产卵)
风门
...
Sagas 是伟大而强大的工具。然而,权力伴随着责任。当您的应用程序增长时,您可能很容易迷失方向,因为您可能会因为弄清楚谁在等待分派动作,或者分派某个动作时发生的一切而迷失方向。另一方面,thunk 更简单,更容易推理。选择一个或另一个取决于许多方面,例如项目的类型和规模、项目必须处理的副作用类型或开发团队的偏好。在任何情况下,只要让您的应用程序简单且可预测。
2020 年 7 月更新:
在过去的 16 个月里,React 社区中最显着的变化可能是 React 钩子。
根据我的观察,为了更好地兼容功能组件和钩子,项目(即使是那些大型项目)倾向于使用:
hook + async thunk(hook 使一切都非常灵活,因此您实际上可以将 async thunk 放置在您想要的位置并将其用作正常功能,例如,仍然在 action.ts 中编写 thunk 然后 useDispatch() 来触发 thunk:https: //stackoverflow.com/a/59991104/5256695)、useRequest、GraphQL/Apollo useQuery useMutation react-fetching-library 其他流行的数据获取/API调用库、工具、设计模式等选择
相比之下,与上述方法相比,redux-saga
在大多数正常的 API 调用情况下并没有真正提供显着优势,同时通过引入许多 saga 文件/生成器增加了项目复杂性(也是因为 { 1} 是在 2019 年 9 月 18 日,那是很久以前的事了)。
但是,redux-saga
仍然提供了一些独特的功能,例如竞速效果和并行请求。因此,如果您需要这些特殊功能,redux-saga
仍然是一个不错的选择。
2019年3月的原帖:
只是一些个人经验:
对于编码风格和可读性,过去使用 redux-saga 最显着的优势之一是避免了 redux-thunk 中的回调地狱——不再需要使用许多嵌套 then/catch。但是现在随着 async/await thunk 的流行,在使用 redux-thunk 的时候也可以写成 sync 风格的 async 代码,这可以看作是对 redux-thunk 的一种改进。在使用 redux-saga 时,可能需要编写更多样板代码,尤其是在 Typescript 中。例如,如果想实现一个 fetch 异步功能,数据和错误处理可以直接在 action.js 中的一个 thunk 单元中执行,只需一个 FETCH 操作。但在 redux-saga 中,可能需要定义 FETCH_START、FETCH_SUCCESS 和 FETCH_FAILURE 动作及其所有相关的类型检查,因为 redux-saga 的特点之一就是使用这种丰富的“令牌”机制来创建效果和指示redux store 方便测试。当然,不使用这些动作也可以编写 saga,但这会使其类似于 thunk。在文件结构方面,redux-saga 在很多情况下似乎更加明确。可以很容易地在每个 sagas.ts 中找到与异步相关的代码,但在 redux-thunk 中,需要在操作中看到它。简单的测试可能是 redux-saga 的另一个重要特征。这真的很方便。但是需要澄清的一点是,redux-saga “调用”测试在测试中不会执行实际的 API 调用,因此需要为 API 调用之后可能使用的步骤指定示例结果。因此,在编写 redux-saga 之前,最好详细规划一个 saga 及其对应的 sagas.spec.ts。 Redux-saga 还提供了许多高级功能,例如并行运行任务、takeLatest/takeEvery、fork/spawn 等并发助手,这些功能远比 thunk 强大。
总之,就个人而言,我想说:在许多正常情况下和中小型应用程序中,使用 async/await 风格的 redux-thunk。它将为您节省许多样板代码/操作/typedef,并且您无需切换许多不同的 sagas.ts 并维护特定的 sagas 树。但是如果你正在开发一个大型应用程序,它具有非常复杂的异步逻辑并且需要并发/并行模式等功能,或者对测试和维护有很高的需求(尤其是在测试驱动开发中),redux-sagas 可能会挽救你的生命.
无论如何,redux-saga 并不比 redux 本身更难更复杂,也没有所谓的陡峭学习曲线,因为它的核心概念和 API 非常有限。花一点时间学习 redux-saga 可能会让你在未来的某一天受益。
根据我的经验,我回顾了几个不同的大型 React/Redux 项目,Sagas 为开发人员提供了一种更结构化的代码编写方式,这种方式更容易测试,也更难出错。
是的,一开始有点奇怪,但大多数开发人员在一天内就对它有足够的了解。我总是告诉人们不要担心 yield
一开始会做什么,一旦你写了几个测试,它就会出现。
我见过几个项目,其中 thunk 被视为来自 MVC 模式的控制器,这很快就变成了无法维护的混乱。
我的建议是在你需要 A 触发 B 类型的与单个事件相关的东西的地方使用 Sagas。对于可能跨越多个动作的任何事情,我发现编写自定义中间件并使用 FSA 动作的元属性来触发它更简单。
Thunks 与 Sagas
Redux-Thunk
和 Redux-Saga
在几个重要方面有所不同,它们都是 Redux 的中间件库(Redux 中间件是拦截通过 dispatch() 方法进入 store 的操作的代码)。
操作实际上可以是任何东西,但如果您遵循最佳实践,则操作是具有类型字段、可选负载、元和错误字段的纯 JavaScript 对象。例如
const loginRequest = {
type: 'LOGIN_REQUEST',
payload: {
name: 'admin',
password: '123',
}, };
Redux-Thunk
除了分派标准操作外,Redux-Thunk
中间件还允许您分派称为 thunks
的特殊函数。
Thunks(在 Redux 中)通常具有以下结构:
export const thunkName =
parameters =>
(dispatch, getState) => {
// Your application logic goes here
};
也就是说,thunk
是一个(可选地)接受一些参数并返回另一个函数的函数。内部函数采用 dispatch function
和 getState
函数——这两个函数都将由 Redux-Thunk
中间件提供。
Redux-Saga
Redux-Saga
中间件允许您将复杂的应用程序逻辑表达为称为 sagas 的纯函数。从测试的角度来看,纯函数是可取的,因为它们是可预测的和可重复的,这使得它们相对容易测试。
Sagas 是通过称为生成器函数的特殊函数实现的。这些是 ES6 JavaScript
的新功能。基本上,在您看到 yield 语句的任何地方,执行都会跳入和跳出生成器。将 yield
语句视为导致生成器暂停并返回产生的值。稍后,调用者可以在 yield
之后的语句处恢复生成器。
生成器函数就是这样定义的。注意 function 关键字后面的星号。
function* mySaga() {
// ...
}
一旦登录传奇注册到 Redux-Saga
。但是第一行的 yield
将暂停 saga,直到类型为 'LOGIN_REQUEST'
的操作被分派到商店。一旦发生这种情况,执行将继续。
For more details see this article。
一个速记。生成器是可取消的,异步/等待 - 不是。因此,对于问题中的一个示例,选择什么并没有真正的意义。但是对于更复杂的流程,有时没有比使用生成器更好的解决方案。
所以,另一个想法可能是使用带有 redux-thunk 的生成器,但对我来说,这似乎是在尝试发明一辆带有方形轮子的自行车。
当然,生成器更容易测试。
这是一个结合了 redux-saga
和 redux-thunk
的最佳部分(优点)的项目:您可以处理 saga 的所有副作用,同时通过 dispatching
相应的操作获得承诺:https://github.com/diegohaz/redux-saga-thunk
class MyComponent extends React.Component {
componentWillMount() {
// `doSomething` dispatches an action which is handled by some saga
this.props.doSomething().then((detail) => {
console.log('Yaay!', detail)
}).catch((error) => {
console.log('Oops!', error)
})
}
}
then()
是违反范式的。您应该在 componentDidUpdate
中处理更改的状态,而不是等待解决承诺。
componentDidlMount() { this.props.doSomething().then((detail) => { this.setState({isReady: true})} }
我最近加入了一个大量使用 redux-saga
的项目,因此也有兴趣了解更多关于 saga 方法的好处。
TBH,我还在寻找。阅读了这篇文章并且很多人都喜欢它,“专业人士”是难以捉摸的。上面的答案似乎总结为:
可测试性(忽略实际的 API 调用)、大量辅助函数、熟悉服务器端编码的开发人员。
许多其他的说法似乎是乐观的、误导的或完全是错误的!例如,我见过许多不合理的说法,例如“笨蛋不能做 X”。但是 thunk 是函数。如果一个函数不能做 X,那么 javascript 就不能做 X。所以 sagas 也不能做 X。
对我来说,缺点是:
使用生成器函数混淆关注点。 JS 中的生成器返回自定义迭代器。就这些。他们没有任何特殊的能力来处理异步调用或被取消。任何循环都可以有中断条件,任何函数都可以处理异步请求,任何代码都可以使用自定义迭代器。当人们说这样的话:生成器可以控制何时监听某些动作或生成器是可取消的,但异步调用不是那么它通过暗示这些品质是生成器函数固有的 - 甚至是独有的 - 造成混乱。
不明确的用例:AFAIK SAGA 模式用于处理跨服务的并发事务问题。鉴于浏览器是单线程的,很难看出并发如何呈现 Promise 方法无法处理的问题。顺便说一句:也很难理解为什么这类问题应该在浏览器中处理。
代码可追溯性:通过使用 redux 中间件将 dispatch 变成一种事件处理,Sagas dispatch action 永远不会到达 reducer,因此永远不会被 Redux 工具记录。虽然其他库也这样做,但它通常是不必要的复杂,因为浏览器内置了事件处理。间接的优势再次难以捉摸,直接调用 saga 时会更加明显。
如果这篇文章让我对 sagas 感到沮丧,那是因为我对 sagas 感到沮丧。它们似乎是寻找要解决的问题的绝佳解决方案。国际海事组织。
更简单的方法是使用 redux-auto。
从文档中
redux-auto 简单地通过允许您创建一个返回承诺的“动作”函数来解决这个异步问题。伴随您的“默认”功能操作逻辑。
不需要其他 Redux 异步中间件。例如 thunk、promise-middleware、saga 轻松地允许您将 promise 传递到 redux 并为您管理它允许您将外部服务调用与它们将被转换的位置放在一起命名文件“init.js”将调用它一次在应用程序启动时。这对于在启动时从服务器加载数据很有用
我们的想法是让每个 action in a specific file。将文件中的服务器调用与“待处理”、“已完成”和“已拒绝”的减速器功能放在一起。这使得处理 Promise 变得非常容易。
它还会自动将 helper object(called "async") 附加到您的状态原型,允许您在 UI 中跟踪请求的转换。
不定期副业成功案例分享
delay
函数来自哪里?啊,找到了:github.com/yelouafi/redux-saga/blob/…redux-thunk
代码可读性强且一目了然。但是redux-sagas
一个真的不可读,主要是因为那些类似动词的功能:call
,fork
,take
,put
......