为什么我们需要中间件来实现 Redux 中的异步流?
Why do we need middleware for async flow in Redux?
根据文档,"Without middleware, Redux store only supports synchronous data flow"。我不明白为什么会这样。为什么容器组件不能调用异步 API,然后 dispatch
动作?
例如,想象一个简单的 UI:一个字段和一个按钮。当用户按下按钮时,该字段会填充来自远程服务器的数据。
import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';
const ActionTypes = {
STARTED_UPDATING: 'STARTED_UPDATING',
UPDATED: 'UPDATED'
};
class AsyncApi {
static getFieldValue() {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(Math.floor(Math.random() * 100));
}, 1000);
});
return promise;
}
}
class App extends React.Component {
render() {
return (
<div>
<input value={this.props.field}/>
<button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
{this.props.isWaiting && <div>Waiting...</div>}
</div>
);
}
}
App.propTypes = {
dispatch: React.PropTypes.func,
field: React.PropTypes.any,
isWaiting: React.PropTypes.bool
};
const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
switch (action.type) {
case ActionTypes.STARTED_UPDATING:
return { ...state, isWaiting: true };
case ActionTypes.UPDATED:
return { ...state, isWaiting: false, field: action.payload };
default:
return state;
}
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
(state) => {
return { ...state };
},
(dispatch) => {
return {
update: () => {
dispatch({
type: ActionTypes.STARTED_UPDATING
});
AsyncApi.getFieldValue()
.then(result => dispatch({
type: ActionTypes.UPDATED,
payload: result
}));
}
};
})(App);
export default class extends React.Component {
render() {
return <Provider store={store}><ConnectedApp/></Provider>;
}
}
渲染导出的组件后,我可以单击按钮并正确更新输入。
注意 connect
调用中的 update
函数。它调度一个动作,告诉应用程序它正在更新,然后执行异步调用。调用完成后,提供的值将作为另一个操作的有效负载进行分派。
这种方法有什么问题?为什么我要像文档中建议的那样使用 Redux Thunk 或 Redux Promise?
编辑: 我在 Redux 存储库中搜索线索,发现 Action Creators 过去必须是纯函数。例如,here's a user trying to provide a better explanation for async data flow:
The action creator itself is still a pure function, but the thunk function it returns doesn't need to be, and it can do our async calls
Action creators are no longer required to be pure.所以说,thunk/promise中间件在以前肯定是必须的,现在好像不是这样了?
简短的回答:对我来说似乎是解决异步问题的一种完全合理的方法。有几个注意事项。
在我们刚开始工作的新项目上工作时,我有非常相似的想法。我是 vanilla Redux 优雅的系统的忠实粉丝,它以一种远离 React 组件树的方式更新商店和重新渲染组件。挂钩到优雅的 dispatch
机制来处理异步对我来说似乎很奇怪。
我最终采用了一种与您在我们项目中提取的库中的方法非常相似的方法,我们称之为 react-redux-controller。
出于以下几个原因,我最终没有采用您上面的确切方法:
- 按照您的编写方式,那些调度函数无法访问商店。您可以通过让 UI 组件传递调度函数所需的所有信息来稍微解决这个问题。但我认为这会将那些 UI 组件不必要地耦合到调度逻辑。更有问题的是,调度函数没有明显的方法来访问异步延续中的更新状态。
- 调度函数可以通过词法范围访问
dispatch
本身。一旦 connect
语句失控,这就限制了重构的选项——而且仅使用一个 update
方法看起来非常笨拙。因此,如果您将这些调度程序函数分解为单独的模块,则需要一些系统来组合它们。
总而言之,您必须安装一些系统以允许 dispatch
将商店以及事件的参数注入到您的调度函数中。我知道这种依赖注入的三种合理方法:
- redux-thunk 通过将它们传递给您的 thunks(根据圆顶定义,使它们根本不完全是 thunks)以功能方式执行此操作。我没有使用其他
dispatch
中间件方法,但我认为它们基本相同。
- react-redux-controller 用协程来做这个。作为奖励,它还允许您访问 "selectors",这些函数可能是您作为第一个参数传递给
connect
的函数,而不必直接使用原始的规范化存储。
- 您也可以通过各种可能的机制将它们注入
this
上下文,以 object-oriented 的方式做到这一点。
更新
我发现这个难题的一部分是 react-redux. The first argument to connect
获取状态快照的限制,但不是调度。第二个参数得到调度但不是状态。两个参数都没有关闭当前状态的 thunk,因为能够在 continuation/callback.
时看到更新的状态
回答开头提出的问题:
Why can't the container component call the async API, and then dispatch the actions?
请记住,这些文档是针对 Redux 的,而不是针对 Redux 加 React 的。 Redux 存储 连接到 React 组件 可以完全按照你说的做,但是没有中间件的 Plain Jane Redux 存储不接受 dispatch
的参数,除了普通的 ol' 对象。
没有中间件你当然可以
const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));
但这是一个类似的情况,异步是 around Redux 而不是 by Redux 处理。因此,中间件通过修改可以直接传递给 dispatch
.
的内容来允许异步
也就是说,我认为您建议的精神是有效的。当然还有其他方法可以在 Redux + React 应用程序中处理异步。
使用中间件的一个好处是您可以像往常一样继续使用动作创建器,而不必担心它们是如何连接的。例如,使用 redux-thunk
,您编写的代码看起来很像
function updateThing() {
return dispatch => {
dispatch({
type: ActionTypes.STARTED_UPDATING
});
AsyncApi.getFieldValue()
.then(result => dispatch({
type: ActionTypes.UPDATED,
payload: result
}));
}
}
const ConnectedApp = connect(
(state) => { ...state },
{ update: updateThing }
)(App);
看起来和原来的没什么不同——只是稍微打乱了一些——而且connect
不知道updateThing
是(或需要)异步的。
如果您还想支持 promises, observables, sagas, or crazy custom and highly declarative 个动作创建者,那么 Redux 只需更改您传递给 dispatch
的内容(也就是您 return 来自动作创建者的内容)就可以做到这一点.无需处理 React 组件(或 connect
调用)。
What is wrong with this approach? Why would I want to use Redux Thunk or Redux Promise, as the documentation suggests?
这种方法没有错。这在大型应用程序中很不方便,因为您将有不同的组件执行相同的操作,您可能希望对某些操作进行去抖动,或者使某些本地状态(如 auto-incrementing ID 接近操作创建者等)。所以它只是从维护的角度更容易将动作创建者提取到单独的函数中。
您可以阅读 以获得更详细的演练。
Redux Thunk 或 Redux Promise 等中间件只是为您提供“语法糖”以分派 thunk 或 promises,但您不必使用它。
因此,如果没有任何中间件,您的动作创建器可能看起来像
// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}
但是使用 Thunk Middleware 你可以这样写:
// action creator
function loadData(userId) {
return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}
所以没有太大的区别。我喜欢后一种方法的一点是组件不关心动作创建者是异步的。它只是正常调用dispatch
,它也可以使用mapDispatchToProps
通过一个简短的语法等来绑定这样的action creator。组件不知道action creator是如何实现的,你可以在不同的async之间切换方法(Redux Thunk、Redux Promise、Redux Saga)而不更改组件。另一方面,使用前一种显式方法,您的组件 确切地 知道特定调用是异步的,并且需要 dispatch
通过某种约定(例如,作为同步参数)。
还要想想这段代码会怎么改。假设我们想要第二个数据加载功能,并将它们组合在一个动作创建器中。
对于第一种方法,我们需要注意我们调用的是哪种动作创建者:
// action creators
function loadSomeData(dispatch, userId) {
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
function loadOtherData(dispatch, userId) {
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
);
}
function loadAllData(dispatch, userId) {
return Promise.all(
loadSomeData(dispatch, userId), // pass dispatch first: it's async
loadOtherData(dispatch, userId) // pass dispatch first: it's async
);
}
// component
componentWillMount() {
loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}
使用 Redux Thunk 动作创建者可以 dispatch
其他动作创建者的结果,甚至不需要考虑它们是同步的还是异步的:
// action creators
function loadSomeData(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
function loadOtherData(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
);
}
function loadAllData(userId) {
return dispatch => Promise.all(
dispatch(loadSomeData(userId)), // just dispatch normally!
dispatch(loadOtherData(userId)) // just dispatch normally!
);
}
// component
componentWillMount() {
this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}
使用这种方法,如果您稍后希望您的操作创建者查看当前的 Redux 状态,您可以只使用传递给 thunk 的第二个 getState
参数,而根本不修改调用代码:
function loadSomeData(userId) {
// Thanks to Redux Thunk I can use getState() here without changing callers
return (dispatch, getState) => {
if (getState().data[userId].isLoaded) {
return Promise.resolve();
}
fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
}
如果需要改成同步,也可以不改任何调用代码:
// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
return {
type: 'LOAD_SOME_DATA_SUCCESS',
data: localStorage.getItem('my-data')
}
}
所以使用像 Redux Thunk 或 Redux Promise 这样的中间件的好处是组件不知道动作创建者是如何实现的,他们是否关心 Redux 状态,他们是同步的还是异步的,以及是否他们打电话给其他动作创造者。缺点是有点间接,但我们相信在实际应用中这是值得的。
最后,Redux Thunk 和朋友只是 Redux 应用程序中异步请求的一种可能方法。另一种有趣的方法是 Redux Saga,它允许您定义 long-running 守护进程(“sagas”),这些守护进程会在操作到来时采取行动,并在输出操作之前转换或执行请求。这将逻辑从动作创建者转移到传奇中。你可能想看看,然后选择最适合你的。
I searched the Redux repo for clues, and found that Action Creators were required to be pure functions in the past.
这是不正确的。文档是这么说的,但是文档错了。
动作创建者从来不需要是纯函数。
我们修复了文档以反映这一点。
你没有。
但是...你应该使用 redux-saga :)
Dan Abramov 关于 redux-thunk
的回答是正确的,但我会多谈谈 redux-saga,它非常相似但更强大。
命令式 VS 声明式
- DOM:jQuery 是命令式的/React 是声明式的
- Monads:IO 是命令式的/Free 是声明式的
- Redux 效果:
redux-thunk
是命令式/redux-saga
是声明式
当你手中有一个 thunk 时,比如 IO monad 或 promise,你无法轻易知道一旦执行它会做什么。测试 thunk 的唯一方法是执行它,并模拟调度程序(或整个外部世界,如果它与更多东西交互......)。
如果你在使用模拟,那么你就不是在进行函数式编程。
Seen through the lens of side-effects, mocks are a flag that your code is impure, and in the functional programmer's eye, proof that something is wrong. Instead of downloading a library to help us check the iceberg is intact, we should be sailing around it.
A hardcore TDD/Java guy once asked me how you do mocking in Clojure. The answer is, we usually don't. We usually see it as a sign we need to refactor our code.
sagas(在 redux-saga
中实现)是声明性的,并且像 Free monad 或 React 组件一样,它们在没有任何模拟的情况下更容易测试。
另请参阅此 article:
in modern FP, we shouldn’t write programs — we should write descriptions of programs, which we can then introspect, transform, and interpret at will.
(实际上,Redux-saga 就像一个混合体:流程是命令式的,但效果是声明式的)
困惑:actions/events/commands...
在前端世界中,对于 CQRS / EventSourcing 和 Flux / Redux 等后端概念之间的关系存在很多混淆,主要是因为在 Flux 中我们使用术语 "action" 有时可以同时表示两者命令式代码 (LOAD_USER
) 和事件 (USER_LOADED
)。我相信像 event-sourcing 一样,您应该只调度事件。
在实践中使用 sagas
想象一个具有 link 用户个人资料的应用程序。用每个中间件处理这个问题的惯用方法是:
redux-thunk
<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>
function loadUserProfile(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
);
}
redux-saga
<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>
function* loadUserProfileOnNameClick() {
yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}
function* fetchUser(action) {
try {
const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
yield put({ type: 'USER_PROFILE_LOADED', userProfile })
}
catch(err) {
yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
}
}
这个传奇转化为:
every time a username gets clicked, fetch the user profile and then dispatch an event with the loaded profile.
如你所见,redux-saga
有一些优点。
takeLatest
的用法表示您只对获取最后一次单击的用户名的数据感兴趣(处理并发问题,以防用户快速单击大量用户名)。这种东西很难用thunks。如果你不想要这种行为,你可以使用 takeEvery
。
你让动作创作者保持纯洁。请注意,保留 actionCreator(在 sagas put
和组件 dispatch
中)仍然很有用,因为它可能会帮助您在将来添加操作验证 (assertions/flow/typescript)。
您的代码变得更加可测试,因为效果是声明性的
您不再需要像 actions.loadUser()
那样触发 rpc-like 调用。您的 UI 只需要发送发生的事情。我们只触发 events(总是过去式!),不再触发动作。这意味着您可以创建解耦 "ducks" or Bounded Contexts 并且 saga 可以充当这些模块化组件之间的耦合点。
这意味着您的视图更易于管理,因为它们不再需要在已发生的事情和应该发生的事情之间包含转换层
例如想象一个无限滚动视图。 CONTAINER_SCROLLED
可以导致 NEXT_PAGE_LOADED
,但是可滚动容器真的有责任决定我们是否应该加载另一个页面吗?然后他必须知道更复杂的事情,比如最后一页是否加载成功,或者是否已经有一个页面试图加载,或者是否没有更多的项目要加载?我不这么认为:为了最大程度的可重用性,可滚动容器应该只描述它已被滚动。页面的加载是那个滚动
的"business effect"
有些人可能会争辩说生成器本质上可以使用局部变量将状态隐藏在 redux 存储之外,但是如果您开始通过启动计时器等在 thunk 中编排复杂的东西,您无论如何都会遇到同样的问题。还有一个 select
效果,现在允许从您的 Redux 存储中获取一些状态。
Sagas 可以是 time-traveled 并且还可以启用复杂的流日志记录和 dev-tools 当前正在处理的内容。这是一些已经实现的简单异步流日志记录:
解耦
Sagas 不仅取代了 redux thunk。它们来自后端/分布式系统/event-sourcing.
这是一个非常普遍的误解,认为 sagas 只是用来取代具有更好可测试性的 redux thunk。实际上这只是 redux-saga 的一个实现细节。在可测试性方面,使用声明式效果比 thunk 更好,但 saga 模式可以在命令式或声明式代码之上实现。
首先,saga 是一个允许协调长 运行 事务(最终一致性)和跨不同有界上下文的事务(领域驱动设计术语)的软件。
为了简化前端世界,假设有 widget1 和 widget2。单击 widget1 上的某个按钮时,它应该对 widget2 有影响。 widget1 没有将 2 个小部件耦合在一起(即 widget1 调度一个针对 widget2 的操作),widget1 仅调度其按钮被单击。然后 saga 侦听此按钮单击,然后通过调度 widget2 知道的新事件来更新 widget2。
这增加了一个间接级别,这对于简单的应用程序来说是不必要的,但可以更轻松地扩展复杂的应用程序。您现在可以将 widget1 和 widget2 发布到不同的 npm 存储库,这样它们就永远不必了解彼此,也无需让它们共享一个全局操作注册表。这 2 个小部件现在是可以单独存在的限界上下文。它们不需要彼此保持一致,也可以在其他应用程序中重复使用。 saga 是两个小部件之间的耦合点,以对您的业务有意义的方式协调它们。
关于如何构建 Redux 应用程序的一些不错的文章,您可以在这些文章上使用 Redux-saga 来解耦:
- http://jaysoo.ca/2016/02/28/organizing-redux-application/
- http://marmelab.com/blog/2015/12/17/react-directory-structure.html
- https://github.com/slorber/scalable-frontend-with-elm-or-redux
具体用例:通知系统
我希望我的组件能够触发 in-app 通知的显示。但我不希望我的组件与具有自己业务规则的通知系统高度耦合(同时显示最多 3 个通知,通知排队,4 秒 display-time 等...)。
我不希望我的 JSX 组件决定通知何时 show/hide。我只是赋予它请求通知的能力,并将复杂的规则留在 saga 中。这种东西很难用 thunk 或 promises 来实现。
我已经描述了如何使用 saga
为什么叫 Saga?
传奇一词来自后端世界。我最初在 long discussion.
中向 Yassine(Redux-saga 的作者)介绍了该术语
最初,该术语是通过 paper 引入的,saga 模式应该用于处理分布式事务中的最终一致性,但它的用法已被后端开发人员扩展到更广泛的定义,因此它现在还涵盖了 "process manager" 模式(原来的 saga 模式在某种程度上是流程管理器的一种特殊形式)。
如今,"saga" 一词令人困惑,因为它可以描述两种不同的事物。正如在 redux-saga 中使用的那样,它描述的不是处理分布式事务的方法,而是一种协调应用程序中的操作的方法。 redux-saga
也可以称为 redux-process-manager
。
另请参阅:
- Interview of Yassine about Redux-saga history
- Kella Byte: Claryfing the Saga pattern
- Microsoft CQRS Journey: A Saga on Sagas
- Medium response of Yassine
备选方案
如果您不喜欢使用生成器的想法,但您对 saga 模式及其解耦属性感兴趣,您也可以使用 redux-observable 实现相同的目的,它使用名称 epic
描述完全相同的模式,但使用 RxJS。如果您已经熟悉 Rx,您会感到宾至如归。
const loadUserProfileOnNameClickEpic = action$ =>
action$.ofType('USER_NAME_CLICKED')
.switchMap(action =>
Observable.ajax(`http://data.com/${action.payload.userId}`)
.map(userProfile => ({
type: 'USER_PROFILE_LOADED',
userProfile
}))
.catch(err => Observable.of({
type: 'USER_PROFILE_LOAD_FAILED',
err
}))
);
一些redux-saga有用的资源
- Managing processes in Redux Saga
- From actionsCreators to Sagas
- Snake game implemented with Redux-saga
2017 年建议
- 不要为了使用而过度使用 Redux-saga。仅可测试 API 调用不值得。
- 对于大多数简单情况,不要从您的项目中删除 thunk。
- 如果有意义,请毫不犹豫地在
yield put(someActionThunk)
中发送 thunk。
如果你害怕使用Redux-saga(或Redux-observable)但只需要解耦模式,检查redux-dispatch-subscribe:它允许监听调度并在监听器中触发新的调度.
const unsubscribe = store.addDispatchListener(action => {
if (action.type === 'ping') {
store.dispatch({ type: 'pong' });
}
});
Abramov 的目标——也是每个人的理想目标——只是 将复杂性(和异步调用)封装在最合适和可重用的地方。
在标准 Redux 数据流中,哪里是执行此操作的最佳位置?怎么样:
- 减速器?决不。它们应该是没有副作用的纯函数。更新商店是一项严肃而复杂的工作。不要污染它。
- Dumb View Components? 绝对不是。他们只关心一个问题:表示和用户交互,并且应该尽可能简单。
- 容器组件? 可能,但不是最佳选择。这是有道理的,因为容器是我们封装一些与视图相关的复杂性并与商店交互的地方,但是:
- 容器确实需要比哑组件更复杂,但它仍然是一个单一的职责:提供视图和 state/store 之间的绑定。您的异步逻辑与此完全不同。
- 通过将它放在一个容器中,您将把异步逻辑锁定到一个上下文中,耦合到一个或多个 views/routes。馊主意。理想情况下,它都是可重用的,并且与视图完全分离。
- (与所有规则一样,如果您有状态绑定逻辑恰好可以在多个上下文中重用,或者如果您可以以某种方式将所有状态概括为类似集成 GraphQL 模式的东西,则可能会有例外。好的,好吧,那可能很酷。但是...大多数时候绑定似乎都非常 context/view 具体。)
- 其他一些服务模块? 坏主意:您需要注入对商店的访问权限,这是一个 maintainability/testability 噩梦。最好使用 Redux 的精髓,只使用提供的 APIs/models 访问商店。
- Action 和解释它们的中间件? 为什么不呢?!对于初学者来说,这是我们剩下的唯一主要选择。 :-) 更合乎逻辑的是,动作系统是分离的执行逻辑,您可以在任何地方使用它。它可以访问商店并可以发送更多操作。它有一个单一的职责,就是围绕应用程序组织控制流和数据流,大多数异步都适合这一点。
- Action Creators 呢?为什么不在那里做异步,而不是在操作本身和中间件中?
- 首先也是最重要的一点,创建者无法像中间件那样访问商店。这意味着您无法分派新的或有操作,无法从商店中读取数据来编写异步等。
- 因此,在必要的地方保持复杂性,并使其他一切保持简单。然后创建者可以是简单的、相对纯净的、易于测试的功能。
OK,我们先来看看中间件是如何工作的,这就很好的回答了问题,这是一个pplyMiddleWare函数的源码在 Redux 中:
function applyMiddleware() {
for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
middlewares[_key] = arguments[_key];
}
return function (createStore) {
return function (reducer, preloadedState, enhancer) {
var store = createStore(reducer, preloadedState, enhancer);
var _dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: function dispatch(action) {
return _dispatch(action);
}
};
chain = middlewares.map(function (middleware) {
return middleware(middlewareAPI);
});
_dispatch = compose.apply(undefined, chain)(store.dispatch);
return _extends({}, store, {
dispatch: _dispatch
});
};
};
}
看这部分,看看我们的dispatch是如何变成function的。
...
getState: store.getState,
dispatch: function dispatch(action) {
return _dispatch(action);
}
- Note that each middleware will be given the
dispatch
and getState
functions as named arguments.
好的,这就是 Redux-thunk 作为 Redux 最常用的中间件之一的自我介绍:
Redux Thunk middleware allows you to write action creators that return
a function instead of an action. The thunk can be used to delay the
dispatch of an action, or to dispatch only if a certain condition is
met. The inner function receives the store methods dispatch and
getState as parameters.
如您所见,它将 return 一个函数而不是一个动作,这意味着您可以随时等待并调用它,因为它是一个函数...
那么thunk到底是什么?维基百科是这样介绍的:
In computer programming, a thunk is a subroutine used to inject an
additional calculation into another subroutine. Thunks are primarily
used to delay a calculation until it is needed, or to insert
operations at the beginning or end of the other subroutine. They have
a variety of other applications to compiler code generation and in
modular programming.
The term originated as a jocular derivative of "think".
A thunk is a function that wraps an expression to delay its
evaluation.
//calculation of 1 + 2 is immediate
//x === 3
let x = 1 + 2;
//calculation of 1 + 2 is delayed
//foo can be called later to perform the calculation
//foo is a thunk!
let foo = () => 1 + 2;
看看这个概念有多简单,以及它如何帮助您管理异步操作...
这是你可以没有它的东西,但请记住在编程中总是有更好、更整洁和正确的方法来做事...
使用Redux-saga是React-redux实现中最好的中间件
例如:
store.js
import createSagaMiddleware from 'redux-saga';
import { createStore, applyMiddleware } from 'redux';
import allReducer from '../reducer/allReducer';
import rootSaga from '../saga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
allReducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga);
export default store;
然后saga.js
import {takeLatest,delay} from 'redux-saga';
import {call, put, take, select} from 'redux-saga/effects';
import { push } from 'react-router-redux';
import data from './data.json';
export function* updateLesson(){
try{
yield put({type:'INITIAL_DATA',payload:data}) // initial data from json
yield* takeLatest('UPDATE_DETAIL',updateDetail) // listen to your action.js
}
catch(e){
console.log("error",e)
}
}
export function* updateDetail(action) {
try{
//To write store update details
}
catch(e){
console.log("error",e)
}
}
export default function* rootSaga(){
yield [
updateLesson()
]
}
然后action.js
export default function updateFruit(props,fruit) {
return (
{
type:"UPDATE_DETAIL",
payload:fruit,
props:props
}
)
}
然后reducer.js
import {combineReducers} from 'redux';
const fetchInitialData = (state=[],action) => {
switch(action.type){
case "INITIAL_DATA":
return ({type:action.type, payload:action.payload});
break;
}
return state;
}
const updateDetailsData = (state=[],action) => {
switch(action.type){
case "INITIAL_DATA":
return ({type:action.type, payload:action.payload});
break;
}
return state;
}
const allReducers =combineReducers({
data:fetchInitialData,
updateDetailsData
})
export default allReducers;
然后main.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app/components/App.jsx';
import {Provider} from 'react-redux';
import store from './app/store';
import createRoutes from './app/routes';
const initialState = {};
const store = configureStore(initialState, browserHistory);
ReactDOM.render(
<Provider store={store}>
<App /> /*is your Component*/
</Provider>,
document.getElementById('app'));
试试这个..有效
先是同步动作创建者,然后是异步动作创建者。
同步动作创建器是这样一种,当我们调用它时,它会立即 return 一个 Action 对象,其中包含附加到该对象的所有相关数据,并准备好由我们的 reducer 处理。
异步动作创建者需要一点时间才能准备好最终派发动作。
根据定义,只要你有一个发出网络请求的动作创建者,它总是有资格成为异步动作创建者。
如果你想在 Redux 应用程序中使用异步操作创建器,你必须安装一个叫做中间件的东西,它可以让你处理那些异步操作创建器。
您可以在告诉我们使用自定义中间件进行异步操作的错误消息中验证这一点。
那么什么是中间件,为什么我们需要它来实现 Redux 中的异步流?
在 redux 中间件(如 redux-thunk)的上下文中,中间件帮助我们处理异步操作创建者,因为这是 Redux 无法开箱即用的东西。
通过将中间件集成到 Redux 循环中,我们仍在调用动作创建器,这将 return 一个将被分派的动作,但现在当我们分派一个动作时,而不是直接发送它对于我们所有的减速器,我们要说的是,一个动作将通过应用程序内所有不同的中间件发送。
在单个 Redux 应用程序中,我们可以根据需要拥有任意数量的中间件。在大多数情况下,在我们从事的项目中,我们会将一两个中间件连接到我们的 Redux 商店。
中间件是一个普通的 JavaScript 函数,我们发送的每一个动作都会调用它。在该功能内部,中间件有机会阻止将动作分派给任何 reducer,它可以修改动作或以任何方式随意处理动作,例如,我们可以创建一个控制台日志的中间件您发出的每一个动作都只是为了您的观赏乐趣。
您可以将大量开源中间件作为依赖项安装到您的项目中。
您不仅限于使用开源中间件或将它们安装为依赖项。您可以编写自己的自定义中间件并在 Redux 存储中使用它。
中间件最流行的用途之一(并得到你的答案)是处理异步动作创建者,可能最流行的中间件是 redux-thunk,它可以帮助你处理异步动作创建者.
还有许多其他类型的中间件也可以帮助您处理异步操作创建者。
回答问题:
Why can't the container component call the async API, and then
dispatch the actions?
我会说至少有两个原因:
第一个原因是关注点分离,调用 api
并取回数据不是 action creator
的工作,您必须将两个参数传递给 action creator function
、action type
和 payload
.
第二个原因是因为 redux store
正在等待一个具有强制操作类型和可选 payload
的普通对象(但在这里你也必须传递有效负载)。
动作创建者应该是如下所示的普通对象:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
并且 Redux-Thunk midleware
的作业 dispache
将您 api call
的结果适当 action
。
在企业项目中工作时,中间件有很多需求,例如 (saga) 在简单的异步流程中不可用,下面是一些:
- 运行 请求并行
- 无需等待即可提取未来的操作
- 非阻塞调用 Race effect, example pickup first
- 响应启动流程排序您的任务(先到先得)
- 作曲
- 任务取消动态分叉任务。
- 支持并发运行 redux 中间件之外的 Saga。
- 使用频道
列表很长,请查看 saga documentation
中的高级部分
Redux 不能 return 函数而不是动作。这只是一个事实。这就是人们使用 Thunk 的原因。阅读这 14 行代码,了解它如何允许异步循环与一些添加的功能分层一起工作:
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;
我会说至少有两个原因:
第一个原因是关注点分离,调用 api 并取回数据不是 action creator 的工作,您必须将两个参数传递给 action creator 函数,操作类型和负载。
第二个原因是因为 redux 存储正在等待一个具有强制操作类型和可选负载的普通对象(但在这里你也必须传递负载)。
动作创建者应该是如下所示的普通对象:
函数添加待办事项(文本){
return{
类型:ADD_TODO,
文本
}
}
Redux-Thunk 中间件的工作是将 api 调用的结果发送给适当的操作。
根据文档,"Without middleware, Redux store only supports synchronous data flow"。我不明白为什么会这样。为什么容器组件不能调用异步 API,然后 dispatch
动作?
例如,想象一个简单的 UI:一个字段和一个按钮。当用户按下按钮时,该字段会填充来自远程服务器的数据。
import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';
const ActionTypes = {
STARTED_UPDATING: 'STARTED_UPDATING',
UPDATED: 'UPDATED'
};
class AsyncApi {
static getFieldValue() {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(Math.floor(Math.random() * 100));
}, 1000);
});
return promise;
}
}
class App extends React.Component {
render() {
return (
<div>
<input value={this.props.field}/>
<button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
{this.props.isWaiting && <div>Waiting...</div>}
</div>
);
}
}
App.propTypes = {
dispatch: React.PropTypes.func,
field: React.PropTypes.any,
isWaiting: React.PropTypes.bool
};
const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
switch (action.type) {
case ActionTypes.STARTED_UPDATING:
return { ...state, isWaiting: true };
case ActionTypes.UPDATED:
return { ...state, isWaiting: false, field: action.payload };
default:
return state;
}
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
(state) => {
return { ...state };
},
(dispatch) => {
return {
update: () => {
dispatch({
type: ActionTypes.STARTED_UPDATING
});
AsyncApi.getFieldValue()
.then(result => dispatch({
type: ActionTypes.UPDATED,
payload: result
}));
}
};
})(App);
export default class extends React.Component {
render() {
return <Provider store={store}><ConnectedApp/></Provider>;
}
}
渲染导出的组件后,我可以单击按钮并正确更新输入。
注意 connect
调用中的 update
函数。它调度一个动作,告诉应用程序它正在更新,然后执行异步调用。调用完成后,提供的值将作为另一个操作的有效负载进行分派。
这种方法有什么问题?为什么我要像文档中建议的那样使用 Redux Thunk 或 Redux Promise?
编辑: 我在 Redux 存储库中搜索线索,发现 Action Creators 过去必须是纯函数。例如,here's a user trying to provide a better explanation for async data flow:
The action creator itself is still a pure function, but the thunk function it returns doesn't need to be, and it can do our async calls
Action creators are no longer required to be pure.所以说,thunk/promise中间件在以前肯定是必须的,现在好像不是这样了?
简短的回答:对我来说似乎是解决异步问题的一种完全合理的方法。有几个注意事项。
在我们刚开始工作的新项目上工作时,我有非常相似的想法。我是 vanilla Redux 优雅的系统的忠实粉丝,它以一种远离 React 组件树的方式更新商店和重新渲染组件。挂钩到优雅的 dispatch
机制来处理异步对我来说似乎很奇怪。
我最终采用了一种与您在我们项目中提取的库中的方法非常相似的方法,我们称之为 react-redux-controller。
出于以下几个原因,我最终没有采用您上面的确切方法:
- 按照您的编写方式,那些调度函数无法访问商店。您可以通过让 UI 组件传递调度函数所需的所有信息来稍微解决这个问题。但我认为这会将那些 UI 组件不必要地耦合到调度逻辑。更有问题的是,调度函数没有明显的方法来访问异步延续中的更新状态。
- 调度函数可以通过词法范围访问
dispatch
本身。一旦connect
语句失控,这就限制了重构的选项——而且仅使用一个update
方法看起来非常笨拙。因此,如果您将这些调度程序函数分解为单独的模块,则需要一些系统来组合它们。
总而言之,您必须安装一些系统以允许 dispatch
将商店以及事件的参数注入到您的调度函数中。我知道这种依赖注入的三种合理方法:
- redux-thunk 通过将它们传递给您的 thunks(根据圆顶定义,使它们根本不完全是 thunks)以功能方式执行此操作。我没有使用其他
dispatch
中间件方法,但我认为它们基本相同。 - react-redux-controller 用协程来做这个。作为奖励,它还允许您访问 "selectors",这些函数可能是您作为第一个参数传递给
connect
的函数,而不必直接使用原始的规范化存储。 - 您也可以通过各种可能的机制将它们注入
this
上下文,以 object-oriented 的方式做到这一点。
更新
我发现这个难题的一部分是 react-redux. The first argument to connect
获取状态快照的限制,但不是调度。第二个参数得到调度但不是状态。两个参数都没有关闭当前状态的 thunk,因为能够在 continuation/callback.
回答开头提出的问题:
Why can't the container component call the async API, and then dispatch the actions?
请记住,这些文档是针对 Redux 的,而不是针对 Redux 加 React 的。 Redux 存储 连接到 React 组件 可以完全按照你说的做,但是没有中间件的 Plain Jane Redux 存储不接受 dispatch
的参数,除了普通的 ol' 对象。
没有中间件你当然可以
const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));
但这是一个类似的情况,异步是 around Redux 而不是 by Redux 处理。因此,中间件通过修改可以直接传递给 dispatch
.
也就是说,我认为您建议的精神是有效的。当然还有其他方法可以在 Redux + React 应用程序中处理异步。
使用中间件的一个好处是您可以像往常一样继续使用动作创建器,而不必担心它们是如何连接的。例如,使用 redux-thunk
,您编写的代码看起来很像
function updateThing() {
return dispatch => {
dispatch({
type: ActionTypes.STARTED_UPDATING
});
AsyncApi.getFieldValue()
.then(result => dispatch({
type: ActionTypes.UPDATED,
payload: result
}));
}
}
const ConnectedApp = connect(
(state) => { ...state },
{ update: updateThing }
)(App);
看起来和原来的没什么不同——只是稍微打乱了一些——而且connect
不知道updateThing
是(或需要)异步的。
如果您还想支持 promises, observables, sagas, or crazy custom and highly declarative 个动作创建者,那么 Redux 只需更改您传递给 dispatch
的内容(也就是您 return 来自动作创建者的内容)就可以做到这一点.无需处理 React 组件(或 connect
调用)。
What is wrong with this approach? Why would I want to use Redux Thunk or Redux Promise, as the documentation suggests?
这种方法没有错。这在大型应用程序中很不方便,因为您将有不同的组件执行相同的操作,您可能希望对某些操作进行去抖动,或者使某些本地状态(如 auto-incrementing ID 接近操作创建者等)。所以它只是从维护的角度更容易将动作创建者提取到单独的函数中。
您可以阅读
Redux Thunk 或 Redux Promise 等中间件只是为您提供“语法糖”以分派 thunk 或 promises,但您不必使用它。
因此,如果没有任何中间件,您的动作创建器可能看起来像
// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}
但是使用 Thunk Middleware 你可以这样写:
// action creator
function loadData(userId) {
return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}
所以没有太大的区别。我喜欢后一种方法的一点是组件不关心动作创建者是异步的。它只是正常调用dispatch
,它也可以使用mapDispatchToProps
通过一个简短的语法等来绑定这样的action creator。组件不知道action creator是如何实现的,你可以在不同的async之间切换方法(Redux Thunk、Redux Promise、Redux Saga)而不更改组件。另一方面,使用前一种显式方法,您的组件 确切地 知道特定调用是异步的,并且需要 dispatch
通过某种约定(例如,作为同步参数)。
还要想想这段代码会怎么改。假设我们想要第二个数据加载功能,并将它们组合在一个动作创建器中。
对于第一种方法,我们需要注意我们调用的是哪种动作创建者:
// action creators
function loadSomeData(dispatch, userId) {
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
function loadOtherData(dispatch, userId) {
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
);
}
function loadAllData(dispatch, userId) {
return Promise.all(
loadSomeData(dispatch, userId), // pass dispatch first: it's async
loadOtherData(dispatch, userId) // pass dispatch first: it's async
);
}
// component
componentWillMount() {
loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}
使用 Redux Thunk 动作创建者可以 dispatch
其他动作创建者的结果,甚至不需要考虑它们是同步的还是异步的:
// action creators
function loadSomeData(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
function loadOtherData(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
);
}
function loadAllData(userId) {
return dispatch => Promise.all(
dispatch(loadSomeData(userId)), // just dispatch normally!
dispatch(loadOtherData(userId)) // just dispatch normally!
);
}
// component
componentWillMount() {
this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}
使用这种方法,如果您稍后希望您的操作创建者查看当前的 Redux 状态,您可以只使用传递给 thunk 的第二个 getState
参数,而根本不修改调用代码:
function loadSomeData(userId) {
// Thanks to Redux Thunk I can use getState() here without changing callers
return (dispatch, getState) => {
if (getState().data[userId].isLoaded) {
return Promise.resolve();
}
fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
}
如果需要改成同步,也可以不改任何调用代码:
// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
return {
type: 'LOAD_SOME_DATA_SUCCESS',
data: localStorage.getItem('my-data')
}
}
所以使用像 Redux Thunk 或 Redux Promise 这样的中间件的好处是组件不知道动作创建者是如何实现的,他们是否关心 Redux 状态,他们是同步的还是异步的,以及是否他们打电话给其他动作创造者。缺点是有点间接,但我们相信在实际应用中这是值得的。
最后,Redux Thunk 和朋友只是 Redux 应用程序中异步请求的一种可能方法。另一种有趣的方法是 Redux Saga,它允许您定义 long-running 守护进程(“sagas”),这些守护进程会在操作到来时采取行动,并在输出操作之前转换或执行请求。这将逻辑从动作创建者转移到传奇中。你可能想看看,然后选择最适合你的。
I searched the Redux repo for clues, and found that Action Creators were required to be pure functions in the past.
这是不正确的。文档是这么说的,但是文档错了。
动作创建者从来不需要是纯函数。
我们修复了文档以反映这一点。
你没有。
但是...你应该使用 redux-saga :)
Dan Abramov 关于 redux-thunk
的回答是正确的,但我会多谈谈 redux-saga,它非常相似但更强大。
命令式 VS 声明式
- DOM:jQuery 是命令式的/React 是声明式的
- Monads:IO 是命令式的/Free 是声明式的
- Redux 效果:
redux-thunk
是命令式/redux-saga
是声明式
当你手中有一个 thunk 时,比如 IO monad 或 promise,你无法轻易知道一旦执行它会做什么。测试 thunk 的唯一方法是执行它,并模拟调度程序(或整个外部世界,如果它与更多东西交互......)。
如果你在使用模拟,那么你就不是在进行函数式编程。
Seen through the lens of side-effects, mocks are a flag that your code is impure, and in the functional programmer's eye, proof that something is wrong. Instead of downloading a library to help us check the iceberg is intact, we should be sailing around it. A hardcore TDD/Java guy once asked me how you do mocking in Clojure. The answer is, we usually don't. We usually see it as a sign we need to refactor our code.
sagas(在 redux-saga
中实现)是声明性的,并且像 Free monad 或 React 组件一样,它们在没有任何模拟的情况下更容易测试。
另请参阅此 article:
in modern FP, we shouldn’t write programs — we should write descriptions of programs, which we can then introspect, transform, and interpret at will.
(实际上,Redux-saga 就像一个混合体:流程是命令式的,但效果是声明式的)
困惑:actions/events/commands...
在前端世界中,对于 CQRS / EventSourcing 和 Flux / Redux 等后端概念之间的关系存在很多混淆,主要是因为在 Flux 中我们使用术语 "action" 有时可以同时表示两者命令式代码 (LOAD_USER
) 和事件 (USER_LOADED
)。我相信像 event-sourcing 一样,您应该只调度事件。
在实践中使用 sagas
想象一个具有 link 用户个人资料的应用程序。用每个中间件处理这个问题的惯用方法是:
redux-thunk
<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>
function loadUserProfile(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
);
}
redux-saga
<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>
function* loadUserProfileOnNameClick() {
yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}
function* fetchUser(action) {
try {
const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
yield put({ type: 'USER_PROFILE_LOADED', userProfile })
}
catch(err) {
yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
}
}
这个传奇转化为:
every time a username gets clicked, fetch the user profile and then dispatch an event with the loaded profile.
如你所见,redux-saga
有一些优点。
takeLatest
的用法表示您只对获取最后一次单击的用户名的数据感兴趣(处理并发问题,以防用户快速单击大量用户名)。这种东西很难用thunks。如果你不想要这种行为,你可以使用 takeEvery
。
你让动作创作者保持纯洁。请注意,保留 actionCreator(在 sagas put
和组件 dispatch
中)仍然很有用,因为它可能会帮助您在将来添加操作验证 (assertions/flow/typescript)。
您的代码变得更加可测试,因为效果是声明性的
您不再需要像 actions.loadUser()
那样触发 rpc-like 调用。您的 UI 只需要发送发生的事情。我们只触发 events(总是过去式!),不再触发动作。这意味着您可以创建解耦 "ducks" or Bounded Contexts 并且 saga 可以充当这些模块化组件之间的耦合点。
这意味着您的视图更易于管理,因为它们不再需要在已发生的事情和应该发生的事情之间包含转换层
例如想象一个无限滚动视图。 CONTAINER_SCROLLED
可以导致 NEXT_PAGE_LOADED
,但是可滚动容器真的有责任决定我们是否应该加载另一个页面吗?然后他必须知道更复杂的事情,比如最后一页是否加载成功,或者是否已经有一个页面试图加载,或者是否没有更多的项目要加载?我不这么认为:为了最大程度的可重用性,可滚动容器应该只描述它已被滚动。页面的加载是那个滚动
有些人可能会争辩说生成器本质上可以使用局部变量将状态隐藏在 redux 存储之外,但是如果您开始通过启动计时器等在 thunk 中编排复杂的东西,您无论如何都会遇到同样的问题。还有一个 select
效果,现在允许从您的 Redux 存储中获取一些状态。
Sagas 可以是 time-traveled 并且还可以启用复杂的流日志记录和 dev-tools 当前正在处理的内容。这是一些已经实现的简单异步流日志记录:
解耦
Sagas 不仅取代了 redux thunk。它们来自后端/分布式系统/event-sourcing.
这是一个非常普遍的误解,认为 sagas 只是用来取代具有更好可测试性的 redux thunk。实际上这只是 redux-saga 的一个实现细节。在可测试性方面,使用声明式效果比 thunk 更好,但 saga 模式可以在命令式或声明式代码之上实现。
首先,saga 是一个允许协调长 运行 事务(最终一致性)和跨不同有界上下文的事务(领域驱动设计术语)的软件。
为了简化前端世界,假设有 widget1 和 widget2。单击 widget1 上的某个按钮时,它应该对 widget2 有影响。 widget1 没有将 2 个小部件耦合在一起(即 widget1 调度一个针对 widget2 的操作),widget1 仅调度其按钮被单击。然后 saga 侦听此按钮单击,然后通过调度 widget2 知道的新事件来更新 widget2。
这增加了一个间接级别,这对于简单的应用程序来说是不必要的,但可以更轻松地扩展复杂的应用程序。您现在可以将 widget1 和 widget2 发布到不同的 npm 存储库,这样它们就永远不必了解彼此,也无需让它们共享一个全局操作注册表。这 2 个小部件现在是可以单独存在的限界上下文。它们不需要彼此保持一致,也可以在其他应用程序中重复使用。 saga 是两个小部件之间的耦合点,以对您的业务有意义的方式协调它们。
关于如何构建 Redux 应用程序的一些不错的文章,您可以在这些文章上使用 Redux-saga 来解耦:
- http://jaysoo.ca/2016/02/28/organizing-redux-application/
- http://marmelab.com/blog/2015/12/17/react-directory-structure.html
- https://github.com/slorber/scalable-frontend-with-elm-or-redux
具体用例:通知系统
我希望我的组件能够触发 in-app 通知的显示。但我不希望我的组件与具有自己业务规则的通知系统高度耦合(同时显示最多 3 个通知,通知排队,4 秒 display-time 等...)。
我不希望我的 JSX 组件决定通知何时 show/hide。我只是赋予它请求通知的能力,并将复杂的规则留在 saga 中。这种东西很难用 thunk 或 promises 来实现。
我已经描述了
为什么叫 Saga?
传奇一词来自后端世界。我最初在 long discussion.
中向 Yassine(Redux-saga 的作者)介绍了该术语最初,该术语是通过 paper 引入的,saga 模式应该用于处理分布式事务中的最终一致性,但它的用法已被后端开发人员扩展到更广泛的定义,因此它现在还涵盖了 "process manager" 模式(原来的 saga 模式在某种程度上是流程管理器的一种特殊形式)。
如今,"saga" 一词令人困惑,因为它可以描述两种不同的事物。正如在 redux-saga 中使用的那样,它描述的不是处理分布式事务的方法,而是一种协调应用程序中的操作的方法。 redux-saga
也可以称为 redux-process-manager
。
另请参阅:
- Interview of Yassine about Redux-saga history
- Kella Byte: Claryfing the Saga pattern
- Microsoft CQRS Journey: A Saga on Sagas
- Medium response of Yassine
备选方案
如果您不喜欢使用生成器的想法,但您对 saga 模式及其解耦属性感兴趣,您也可以使用 redux-observable 实现相同的目的,它使用名称 epic
描述完全相同的模式,但使用 RxJS。如果您已经熟悉 Rx,您会感到宾至如归。
const loadUserProfileOnNameClickEpic = action$ =>
action$.ofType('USER_NAME_CLICKED')
.switchMap(action =>
Observable.ajax(`http://data.com/${action.payload.userId}`)
.map(userProfile => ({
type: 'USER_PROFILE_LOADED',
userProfile
}))
.catch(err => Observable.of({
type: 'USER_PROFILE_LOAD_FAILED',
err
}))
);
一些redux-saga有用的资源
- Managing processes in Redux Saga
- From actionsCreators to Sagas
- Snake game implemented with Redux-saga
2017 年建议
- 不要为了使用而过度使用 Redux-saga。仅可测试 API 调用不值得。
- 对于大多数简单情况,不要从您的项目中删除 thunk。
- 如果有意义,请毫不犹豫地在
yield put(someActionThunk)
中发送 thunk。
如果你害怕使用Redux-saga(或Redux-observable)但只需要解耦模式,检查redux-dispatch-subscribe:它允许监听调度并在监听器中触发新的调度.
const unsubscribe = store.addDispatchListener(action => {
if (action.type === 'ping') {
store.dispatch({ type: 'pong' });
}
});
Abramov 的目标——也是每个人的理想目标——只是 将复杂性(和异步调用)封装在最合适和可重用的地方。
在标准 Redux 数据流中,哪里是执行此操作的最佳位置?怎么样:
- 减速器?决不。它们应该是没有副作用的纯函数。更新商店是一项严肃而复杂的工作。不要污染它。
- Dumb View Components? 绝对不是。他们只关心一个问题:表示和用户交互,并且应该尽可能简单。
- 容器组件? 可能,但不是最佳选择。这是有道理的,因为容器是我们封装一些与视图相关的复杂性并与商店交互的地方,但是:
- 容器确实需要比哑组件更复杂,但它仍然是一个单一的职责:提供视图和 state/store 之间的绑定。您的异步逻辑与此完全不同。
- 通过将它放在一个容器中,您将把异步逻辑锁定到一个上下文中,耦合到一个或多个 views/routes。馊主意。理想情况下,它都是可重用的,并且与视图完全分离。
- (与所有规则一样,如果您有状态绑定逻辑恰好可以在多个上下文中重用,或者如果您可以以某种方式将所有状态概括为类似集成 GraphQL 模式的东西,则可能会有例外。好的,好吧,那可能很酷。但是...大多数时候绑定似乎都非常 context/view 具体。)
- 其他一些服务模块? 坏主意:您需要注入对商店的访问权限,这是一个 maintainability/testability 噩梦。最好使用 Redux 的精髓,只使用提供的 APIs/models 访问商店。
- Action 和解释它们的中间件? 为什么不呢?!对于初学者来说,这是我们剩下的唯一主要选择。 :-) 更合乎逻辑的是,动作系统是分离的执行逻辑,您可以在任何地方使用它。它可以访问商店并可以发送更多操作。它有一个单一的职责,就是围绕应用程序组织控制流和数据流,大多数异步都适合这一点。
- Action Creators 呢?为什么不在那里做异步,而不是在操作本身和中间件中?
- 首先也是最重要的一点,创建者无法像中间件那样访问商店。这意味着您无法分派新的或有操作,无法从商店中读取数据来编写异步等。
- 因此,在必要的地方保持复杂性,并使其他一切保持简单。然后创建者可以是简单的、相对纯净的、易于测试的功能。
- Action Creators 呢?为什么不在那里做异步,而不是在操作本身和中间件中?
OK,我们先来看看中间件是如何工作的,这就很好的回答了问题,这是一个pplyMiddleWare函数的源码在 Redux 中:
function applyMiddleware() {
for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
middlewares[_key] = arguments[_key];
}
return function (createStore) {
return function (reducer, preloadedState, enhancer) {
var store = createStore(reducer, preloadedState, enhancer);
var _dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: function dispatch(action) {
return _dispatch(action);
}
};
chain = middlewares.map(function (middleware) {
return middleware(middlewareAPI);
});
_dispatch = compose.apply(undefined, chain)(store.dispatch);
return _extends({}, store, {
dispatch: _dispatch
});
};
};
}
看这部分,看看我们的dispatch是如何变成function的。
...
getState: store.getState,
dispatch: function dispatch(action) {
return _dispatch(action);
}
- Note that each middleware will be given the
dispatch
andgetState
functions as named arguments.
好的,这就是 Redux-thunk 作为 Redux 最常用的中间件之一的自我介绍:
Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methods dispatch and getState as parameters.
如您所见,它将 return 一个函数而不是一个动作,这意味着您可以随时等待并调用它,因为它是一个函数...
那么thunk到底是什么?维基百科是这样介绍的:
In computer programming, a thunk is a subroutine used to inject an additional calculation into another subroutine. Thunks are primarily used to delay a calculation until it is needed, or to insert operations at the beginning or end of the other subroutine. They have a variety of other applications to compiler code generation and in modular programming.
The term originated as a jocular derivative of "think".
A thunk is a function that wraps an expression to delay its evaluation.
//calculation of 1 + 2 is immediate
//x === 3
let x = 1 + 2;
//calculation of 1 + 2 is delayed
//foo can be called later to perform the calculation
//foo is a thunk!
let foo = () => 1 + 2;
看看这个概念有多简单,以及它如何帮助您管理异步操作...
这是你可以没有它的东西,但请记住在编程中总是有更好、更整洁和正确的方法来做事...
使用Redux-saga是React-redux实现中最好的中间件
例如: store.js
import createSagaMiddleware from 'redux-saga';
import { createStore, applyMiddleware } from 'redux';
import allReducer from '../reducer/allReducer';
import rootSaga from '../saga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
allReducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga);
export default store;
然后saga.js
import {takeLatest,delay} from 'redux-saga';
import {call, put, take, select} from 'redux-saga/effects';
import { push } from 'react-router-redux';
import data from './data.json';
export function* updateLesson(){
try{
yield put({type:'INITIAL_DATA',payload:data}) // initial data from json
yield* takeLatest('UPDATE_DETAIL',updateDetail) // listen to your action.js
}
catch(e){
console.log("error",e)
}
}
export function* updateDetail(action) {
try{
//To write store update details
}
catch(e){
console.log("error",e)
}
}
export default function* rootSaga(){
yield [
updateLesson()
]
}
然后action.js
export default function updateFruit(props,fruit) {
return (
{
type:"UPDATE_DETAIL",
payload:fruit,
props:props
}
)
}
然后reducer.js
import {combineReducers} from 'redux';
const fetchInitialData = (state=[],action) => {
switch(action.type){
case "INITIAL_DATA":
return ({type:action.type, payload:action.payload});
break;
}
return state;
}
const updateDetailsData = (state=[],action) => {
switch(action.type){
case "INITIAL_DATA":
return ({type:action.type, payload:action.payload});
break;
}
return state;
}
const allReducers =combineReducers({
data:fetchInitialData,
updateDetailsData
})
export default allReducers;
然后main.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app/components/App.jsx';
import {Provider} from 'react-redux';
import store from './app/store';
import createRoutes from './app/routes';
const initialState = {};
const store = configureStore(initialState, browserHistory);
ReactDOM.render(
<Provider store={store}>
<App /> /*is your Component*/
</Provider>,
document.getElementById('app'));
试试这个..有效
先是同步动作创建者,然后是异步动作创建者。
同步动作创建器是这样一种,当我们调用它时,它会立即 return 一个 Action 对象,其中包含附加到该对象的所有相关数据,并准备好由我们的 reducer 处理。
异步动作创建者需要一点时间才能准备好最终派发动作。
根据定义,只要你有一个发出网络请求的动作创建者,它总是有资格成为异步动作创建者。
如果你想在 Redux 应用程序中使用异步操作创建器,你必须安装一个叫做中间件的东西,它可以让你处理那些异步操作创建器。
您可以在告诉我们使用自定义中间件进行异步操作的错误消息中验证这一点。
那么什么是中间件,为什么我们需要它来实现 Redux 中的异步流?
在 redux 中间件(如 redux-thunk)的上下文中,中间件帮助我们处理异步操作创建者,因为这是 Redux 无法开箱即用的东西。
通过将中间件集成到 Redux 循环中,我们仍在调用动作创建器,这将 return 一个将被分派的动作,但现在当我们分派一个动作时,而不是直接发送它对于我们所有的减速器,我们要说的是,一个动作将通过应用程序内所有不同的中间件发送。
在单个 Redux 应用程序中,我们可以根据需要拥有任意数量的中间件。在大多数情况下,在我们从事的项目中,我们会将一两个中间件连接到我们的 Redux 商店。
中间件是一个普通的 JavaScript 函数,我们发送的每一个动作都会调用它。在该功能内部,中间件有机会阻止将动作分派给任何 reducer,它可以修改动作或以任何方式随意处理动作,例如,我们可以创建一个控制台日志的中间件您发出的每一个动作都只是为了您的观赏乐趣。
您可以将大量开源中间件作为依赖项安装到您的项目中。
您不仅限于使用开源中间件或将它们安装为依赖项。您可以编写自己的自定义中间件并在 Redux 存储中使用它。
中间件最流行的用途之一(并得到你的答案)是处理异步动作创建者,可能最流行的中间件是 redux-thunk,它可以帮助你处理异步动作创建者.
还有许多其他类型的中间件也可以帮助您处理异步操作创建者。
回答问题:
Why can't the container component call the async API, and then dispatch the actions?
我会说至少有两个原因:
第一个原因是关注点分离,调用 api
并取回数据不是 action creator
的工作,您必须将两个参数传递给 action creator function
、action type
和 payload
.
第二个原因是因为 redux store
正在等待一个具有强制操作类型和可选 payload
的普通对象(但在这里你也必须传递有效负载)。
动作创建者应该是如下所示的普通对象:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
并且 Redux-Thunk midleware
的作业 dispache
将您 api call
的结果适当 action
。
在企业项目中工作时,中间件有很多需求,例如 (saga) 在简单的异步流程中不可用,下面是一些:
- 运行 请求并行
- 无需等待即可提取未来的操作
- 非阻塞调用 Race effect, example pickup first
- 响应启动流程排序您的任务(先到先得)
- 作曲
- 任务取消动态分叉任务。
- 支持并发运行 redux 中间件之外的 Saga。
- 使用频道
列表很长,请查看 saga documentation
中的高级部分Redux 不能 return 函数而不是动作。这只是一个事实。这就是人们使用 Thunk 的原因。阅读这 14 行代码,了解它如何允许异步循环与一些添加的功能分层一起工作:
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;
我会说至少有两个原因:
第一个原因是关注点分离,调用 api 并取回数据不是 action creator 的工作,您必须将两个参数传递给 action creator 函数,操作类型和负载。
第二个原因是因为 redux 存储正在等待一个具有强制操作类型和可选负载的普通对象(但在这里你也必须传递负载)。
动作创建者应该是如下所示的普通对象:
函数添加待办事项(文本){ return{ 类型:ADD_TODO, 文本 } } Redux-Thunk 中间件的工作是将 api 调用的结果发送给适当的操作。