我有一个更新应用程序通知状态的操作。通常,此通知将是某种错误或信息。然后我需要在 5 秒后调度另一个动作,将通知状态返回到初始状态,因此没有通知。这背后的主要原因是提供通知在 5 秒后自动消失的功能。
我没有运气使用 setTimeout
并返回另一个动作,并且无法在线找到这是如何完成的。所以欢迎任何建议。
原文由 Ilja 发布,翻译遵循 CC BY-SA 4.0 许可协议
我有一个更新应用程序通知状态的操作。通常,此通知将是某种错误或信息。然后我需要在 5 秒后调度另一个动作,将通知状态返回到初始状态,因此没有通知。这背后的主要原因是提供通知在 5 秒后自动消失的功能。
我没有运气使用 setTimeout
并返回另一个动作,并且无法在线找到这是如何完成的。所以欢迎任何建议。
原文由 Ilja 发布,翻译遵循 CC BY-SA 4.0 许可协议
正如 Dan Abramov 所说,如果您想对异步代码进行更高级的控制,您可以看看 redux-saga 。
这个答案是一个简单的例子,如果你想更好地解释为什么 redux-saga 对你的应用程序有用,请查看 这个其他答案。
一般的想法是 Redux-saga 提供了一个 ES6 生成器解释器,允许您轻松编写看起来像同步代码的异步代码(这就是为什么您经常会在 Redux-saga 中发现无限 while 循环)。不知何故,Redux-saga 正在直接在 Javascript 中构建自己的语言。 Redux-saga 一开始可能会觉得有点难学,因为你需要对生成器有基本的了解,还要了解 Redux-saga 提供的语言。
我将在这里尝试描述我在 redux-saga 之上构建的通知系统。此示例当前正在生产中运行。
我的生产应用程序 Stample.co 的屏幕截图
在这里,我将通知命名为 a toast
但这是一个命名细节。
function* toastSaga() {
// Some config constants
const MaxToasts = 3;
const ToastDisplayTime = 4000;
// Local generator state: you can put this state in Redux store
// if it's really important to you, in my case it's not really
let pendingToasts = []; // A queue of toasts waiting to be displayed
let activeToasts = []; // Toasts currently displayed
// Trigger the display of a toast for 4 seconds
function* displayToast(toast) {
if ( activeToasts.length >= MaxToasts ) {
throw new Error("can't display more than " + MaxToasts + " at the same time");
}
activeToasts = [...activeToasts,toast]; // Add to active toasts
yield put(events.toastDisplayed(toast)); // Display the toast (put means dispatch)
yield call(delay,ToastDisplayTime); // Wait 4 seconds
yield put(events.toastHidden(toast)); // Hide the toast
activeToasts = _.without(activeToasts,toast); // Remove from active toasts
}
// Everytime we receive a toast display request, we put that request in the queue
function* toastRequestsWatcher() {
while ( true ) {
// Take means the saga will block until TOAST_DISPLAY_REQUESTED action is dispatched
const event = yield take(Names.TOAST_DISPLAY_REQUESTED);
const newToast = event.data.toastData;
pendingToasts = [...pendingToasts,newToast];
}
}
// We try to read the queued toasts periodically and display a toast if it's a good time to do so...
function* toastScheduler() {
while ( true ) {
const canDisplayToast = activeToasts.length < MaxToasts && pendingToasts.length > 0;
if ( canDisplayToast ) {
// We display the first pending toast of the queue
const [firstToast,...remainingToasts] = pendingToasts;
pendingToasts = remainingToasts;
// Fork means we are creating a subprocess that will handle the display of a single toast
yield fork(displayToast,firstToast);
// Add little delay so that 2 concurrent toast requests aren't display at the same time
yield call(delay,300);
}
else {
yield call(delay,50);
}
}
}
// This toast saga is a composition of 2 smaller "sub-sagas" (we could also have used fork/spawn effects here, the difference is quite subtile: it depends if you want toastSaga to block)
yield [
call(toastRequestsWatcher),
call(toastScheduler)
]
}
和减速器:
const reducer = (state = [],event) => {
switch (event.name) {
case Names.TOAST_DISPLAYED:
return [...state,event.data.toastData];
case Names.TOAST_HIDDEN:
return _.without(state,event.data.toastData);
default:
return state;
}
};
您可以简单地调度 TOAST_DISPLAY_REQUESTED
事件。如果发送 4 个请求,则只会显示 3 个通知,第 1 个通知消失后,第 4 个会稍晚出现。
请注意,我不特别推荐从 JSX 调度 TOAST_DISPLAY_REQUESTED
。您宁愿添加另一个 saga 来侦听您已经存在的应用程序事件,然后调度 TOAST_DISPLAY_REQUESTED
:触发通知的组件不必与通知系统紧密耦合。
我的代码并不完美,但在生产环境中运行了几个月,错误为 0。 Redux-saga 和生成器最初有点难,但一旦你理解了它们,这种系统就很容易构建。
实现更复杂的规则甚至非常容易,例如:
老实说,祝你好运,用 thunks 正确地实现这种东西。
请注意,您可以使用与 redux-saga 非常相似的 redux-observable 做完全相同的事情。它几乎相同,只是生成器和 RxJS 之间的品味问题。
原文由 Sebastien Lorber 发布,翻译遵循 CC BY-SA 4.0 许可协议
13 回答13k 阅读
7 回答2.1k 阅读
3 回答1.3k 阅读✓ 已解决
2 回答1.4k 阅读✓ 已解决
6 回答1.2k 阅读✓ 已解决
6 回答1.1k 阅读
3 回答1.3k 阅读✓ 已解决
不要陷入 认为图书馆应该规定如何做所有事情 的陷阱。如果你想在 JavaScript 中做一些超时的事情,你需要使用
setTimeout
。 Redux 操作没有任何不同的理由。Redux 确实 提供了一些处理异步内容的替代方法,但只有在意识到重复太多代码时才应该使用这些方法。除非您遇到此问题,否则请使用该语言提供的内容并寻求最简单的解决方案。
内联编写异步代码
这是迄今为止最简单的方法。这里并没有什么特定于 Redux 的。
同样,从连接组件内部:
唯一的区别是,在连接的组件中,您通常无法访问 store 本身,而是将
dispatch()
或特定的 action creators 作为 props 注入。然而,这对我们没有任何影响。如果您不喜欢在从不同组件调度相同的动作时打错字,您可能想要提取动作创建者而不是内联调度动作对象:
或者,如果您之前已将它们与
connect()
绑定:到目前为止,我们还没有使用任何中间件或其他高级概念。
提取异步动作创建者
上面的方法在简单的情况下可以正常工作,但您可能会发现它存在一些问题:
它迫使您在要显示通知的任何地方复制此逻辑。
通知没有 ID,因此如果您足够快地显示两个通知,您将有一个竞争条件。当第一个超时结束时,它将调度
HIDE_NOTIFICATION
,错误地隐藏第二个通知,而不是在超时之后。要解决这些问题,您需要提取一个函数来集中超时逻辑并分派这两个操作。它可能看起来像这样:
现在组件可以使用
showNotificationWithTimeout
,而无需重复此逻辑或具有不同通知的竞争条件:为什么
showNotificationWithTimeout()
接受dispatch
作为第一个参数?因为它需要向 store 发送操作。通常一个组件可以访问dispatch
,但由于我们希望一个外部函数来控制调度,我们需要赋予它对调度的控制权。如果你有一个从某个模块导出的单例存储,你可以直接导入它并直接在其上
dispatch
:这看起来更简单,但 我们不推荐这种方法。我们不喜欢它的主要原因是因为 它迫使 store 成为单例。这使得实现 服务器渲染 变得非常困难。在服务器上,您会希望每个请求都有自己的存储,以便不同的用户获得不同的预加载数据。
单例商店也使测试变得更加困难。在测试动作创建者时,您不能再模拟商店,因为它们引用了从特定模块导出的特定真实商店。您甚至无法从外部重置其状态。
因此,虽然您在技术上可以从模块中导出单例存储,但我们不鼓励这样做。除非您确定您的应用永远不会添加服务器渲染,否则不要这样做。
回到以前的版本:
这解决了逻辑重复的问题,并使我们免于竞争条件。
Thunk 中间件
对于简单的应用程序,该方法就足够了。如果您对中间件感到满意,请不要担心它。
但是,在较大的应用程序中,您可能会发现一些不便之处。
例如,我们不得不传递
dispatch
似乎很不幸。这使得 分离容器和展示组件 变得更加棘手,因为以上述方式异步调度 Redux 操作的任何组件都必须接受dispatch
作为道具,以便它可以进一步传递它。您不能再将动作创建者与connect()
绑定,因为showNotificationWithTimeout()
并不是真正的动作创建者。它不返回 Redux 操作。此外,很难记住哪些函数是像
showNotification()
showNotificationWithTimeout()
的异步助手。您必须以不同的方式使用它们,并注意不要将它们误认为是彼此。这就是 寻找一种方法来“合法化”这种向辅助函数提供
dispatch
的模式的动机,并帮助 Redux 将这种异步动作创建者“视为”普通动作创建者的特例, 而不是完全不同的函数。如果您仍然和我们在一起,并且您还发现您的应用程序存在问题,那么欢迎您使用 Redux Thunk 中间件。
概括地说,Redux Thunk 教 Redux 识别实际上是函数的特殊类型的操作:
启用此中间件后, 如果您调度一个函数,Redux Thunk 中间件会将其作为参数进行
dispatch
。它也会“吞下”这样的动作,所以不用担心你的 reducer 会收到奇怪的函数参数。你的 reducer 只会接收普通的对象动作——要么直接发出,要么由我们刚刚描述的函数发出。这看起来不是很有用,不是吗?不是在这种特殊情况下。然而,它让我们可以
showNotificationWithTimeout()
声明为常规的 Redux 动作创建者:请注意,该函数与我们在上一节中编写的函数几乎相同。但是它不接受
dispatch
作为第一个参数。相反,它 返回 一个接受dispatch
作为第一个参数的函数。我们将如何在我们的组件中使用它?当然,我们可以这样写:
我们正在调用异步操作创建者来获取只需要
dispatch
的内部函数,然后我们传递dispatch
。然而这比原版更尴尬!我们为什么要走那条路?
因为我之前告诉过你。 如果启用了 Redux Thunk 中间件,任何时候你尝试调度一个函数而不是一个动作对象,中间件都会以
dispatch
方法本身作为第一个参数来调用该函数。所以我们可以这样做:
最后,调度一个异步动作(实际上是一系列动作)看起来与将单个动作同步调度到组件没有什么不同。这很好,因为组件不应该关心某些事情是同步发生还是异步发生。我们只是把它抽象掉了。
请注意,由于我们“教”了 Redux 识别这些“特殊”动作创建者(我们称它们为 thunk 动作创建者),我们现在可以在任何使用常规动作创建者的地方使用它们。例如,我们可以将它们与
connect()
一起使用:Thunks 中的读取状态
通常你的 reducer 包含用于确定下一个状态的业务逻辑。但是,reducer 仅在动作被调度后才开始。如果您在 thunk 动作创建器中有副作用(例如调用 API),并且您想在某些情况下阻止它,该怎么办?
如果不使用 thunk 中间件,您只需在组件内部进行以下检查:
但是,提取动作创建者的目的是将这种重复逻辑集中在许多组件中。幸运的是,Redux Thunk 为您提供了一种 读取 Redux 存储当前状态的方法。除了
dispatch
,它还将getState
作为第二个参数传递给您从 thunk 动作创建者返回的函数。这让 thunk 读取存储的当前状态。不要滥用这种模式。当有可用的缓存数据时,它有利于摆脱 API 调用,但它不是构建业务逻辑的一个很好的基础。如果您仅使用
getState()
有条件地分派不同的操作,请考虑将业务逻辑放入减速器中。下一步
现在您已经对 thunk 的工作原理有了基本的了解,请查看使用它们的 Redux 异步示例。
您可能会发现许多 thunk 返回 Promises 的示例。这不是必需的,但非常方便。 Redux 不关心你从一个 thunk 中返回什么,但它从
dispatch()
给你它的返回值。这就是为什么您可以通过调用dispatch(someThunkReturningPromise()).then(...)
从 thunk 返回 Promise 并等待它完成的原因。您还可以将复杂的 thunk 动作创建者拆分为几个较小的 thunk 动作创建者。 thunks 提供的
dispatch
方法可以接受 thunk 本身,因此您可以递归地应用该模式。同样,这对 Promises 最有效,因为您可以在此基础上实现异步控制流。对于某些应用程序,您可能会发现自己处于异步控制流要求过于复杂而无法用 thunk 表达的情况。例如,以这种方式编写时,重试失败的请求、使用令牌的重新授权流程或分步入职可能过于冗长且容易出错。在这种情况下,您可能希望查看更高级的异步控制流解决方案,例如 Redux Saga 或 Redux Loop 。评估它们,比较与您的需求相关的示例,然后选择您最喜欢的示例。
最后,如果您没有真正需要它们,请不要使用任何东西(包括 thunk)。请记住,根据要求,您的解决方案可能看起来很简单
除非您知道自己为什么要这样做,否则不要出汗。