当前位置: 首页 > 知识库问答 >
问题:

如何调度一个超时的Redux操作?

杜俭
2023-03-14

我有一个更新应用程序通知状态的操作。通常,此通知将是一个错误或某种类型的信息。然后我需要在5秒后发送另一个动作,它将通知状态返回到初始状态,所以没有通知。这背后的主要原因是提供通知在5秒后自动消失的功能。

我没有使用settimeout返回另一个操作,也找不到这是如何在网上完成的。所以欢迎任何建议。

共有2个答案

滑弘扬
2023-03-14

正如Dan Abramov所说的,如果您想要对异步代码进行更高级的控制,您可以看看Redux-Saga。

这个答案是一个简单的例子,如果您想更好地解释为什么redux-saga对您的应用程序有用,请检查这个答案。

一般的想法是,Redux-saga提供了一个ES6生成器解释器,它允许您轻松地编写看起来像同步代码的异步代码(这就是为什么您经常在Redux-saga中发现无限while循环的原因)。不知何故,Redux-saga直接在JavaScript内部构建自己的语言。Redux-saga一开始会觉得有点难学,因为您需要对生成器有基本的了解,但也要理解Redux-saga提供的语言。

我将尝试在这里描述我在Redux-Saga之上构建的通知系统。此示例当前正在生产中运行。

  • 您可以请求显示通知
  • 您可以请求通知隐藏
  • 通知的显示时间不应超过4秒
  • 可以同时显示多个通知
  • 最多只能同时显示3个通知
  • 如果在已显示3个通知的情况下请求通知,则将其排队/推迟。

我的制作app stample.co截图

这里我将通知命名为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_request事件。如果您分派了4个请求,那么只会显示3个通知,一旦第一个通知消失,第四个通知就会稍晚一点出现。

注意,我并不特别推荐从JSX调度toast_display_request。您宁愿添加另一个侦听已经存在的应用程序事件的saga,然后分派toast_display_request:触发通知的组件不必与通知系统紧密耦合。

我的代码不是完美的,但在生产中运行了几个月没有bug。Redux-saga和generators最初有点难,但一旦你理解了它们,这种系统就很容易构建了。

实现更复杂的规则也相当容易,比如:

  • 当“排队”的通知太多时,为每个通知提供较少的显示时间,以便队列大小可以更快地减小。
  • 检测窗口大小更改,并相应地更改显示通知的最大数量(例如,桌面=3,电话肖像=2,电话景观=1)

老实说,祝你好运,用Thunks正确地实现这种东西。

注意,您可以使用redux-observable做完全相同的事情,它与Redux-Saga非常相似。这几乎是相同的,是一个问题的味道之间的发电机和RXJ。

松雅昶
2023-03-14

不要落入这样的陷阱:图书馆应该规定如何做每件事。如果您希望在JavaScript中使用超时执行某些操作,则需要使用settimeout。Redux操作没有任何不同的理由。

Redux确实提供了一些处理异步内容的替代方法,但只有当您意识到重复的代码太多时才应该使用这些方法。除非您有这个问题,否则请使用该语言提供的内容并寻求最简单的解决方案。

这是迄今为止最简单的方法。这里没有什么特别的东西可以还原。

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

类似地,从连接的组件内部:

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

唯一的区别是,在连接的组件中,您通常不能访问存储本身,而是获得dispatch()或作为道具注入的特定操作创建者。然而,这对我们来说并没有什么不同。

如果您不喜欢在从不同组件分派相同操作时出错,您可能希望提取操作创建者,而不是内联分派操作对象:

// actions.js
export function showNotification(text) {
  return { type: 'SHOW_NOTIFICATION', text }
}
export function hideNotification() {
  return { type: 'HIDE_NOTIFICATION' }
}

// component.js
import { showNotification, hideNotification } from '../actions'

this.props.dispatch(showNotification('You just logged in.'))
setTimeout(() => {
  this.props.dispatch(hideNotification())
}, 5000)

或者,如果您以前使用connect()绑定了它们:

this.props.showNotification('You just logged in.')
setTimeout(() => {
  this.props.hideNotification()
}, 5000)

到目前为止,我们还没有使用任何中间件或其他先进的概念。

上面的方法在简单的情况下工作很好,但是您可能会发现它有一些问题:

  • 它强制您在任何要显示通知的地方重复此逻辑。
  • 通知没有ID,因此如果您显示两个通知的速度足够快,就会出现争用情况。当第一次超时结束时,它将发送hide_notification,错误地将第二次通知隐藏得早于超时之后。

为了解决这些问题,您需要提取一个函数来集中超时逻辑并调度这两个操作。它看起来可能是这样的:

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  // Assigning IDs to notifications lets reducer ignore HIDE_NOTIFICATION
  // for the notification that is not currently visible.
  // Alternatively, we could store the timeout ID and call
  // clearTimeout(), but we’d still want to do it in a single place.
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

现在组件可以使用ShowNotificationWithTimeout,而无需重复此逻辑或使用不同通知具有竞争条件:

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

为什么shownotificationwithtimeout()接受dispatch作为第一个参数?因为它需要将操作分派到存储区。通常,组件可以访问dispatch,但由于我们希望外部函数控制调度,因此我们需要让它控制调度。

如果您有一个从某个模块导出的单例存储,您可以直接导入它,并在它上直接添加dispatch:

// store.js
export default createStore(reducer)

// actions.js
import store from './store'

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  const id = nextNotificationId++
  store.dispatch(showNotification(id, text))

  setTimeout(() => {
    store.dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout('You just logged in.')

// otherComponent.js
showNotificationWithTimeout('You just logged out.')    

这看起来更简单,但我们不推荐这种方法。我们不喜欢它的主要原因是因为它迫使商店是一个单一的。这使得实现服务器呈现非常困难。在服务器上,您将希望每个请求都有自己的存储,这样不同的用户获得不同的预加载数据。

一个单一的商店也使得测试更加困难。在测试操作创建器时,您不能再模拟存储,因为它们引用了从特定模块导出的特定真实存储。你甚至不能从外部重置它的状态。

因此,虽然技术上可以从模块导出单例存储,但我们不鼓励这样做。除非你确定你的应用永远不会添加服务器呈现,否则不要这样做。

返回以前的版本:

// actions.js

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

这解决了逻辑重复的问题,并使我们免于竞争条件。

对于简单的应用程序,这种方法应该就足够了。如果您对中间件感到满意,就不要担心它。

然而,在较大的应用程序中,您可能会发现一些不方便之处。

例如,似乎不幸的是,我们不得不四处传递dispatch。这使得分离容器和表示组件变得更加棘手,因为以上述方式异步调度Redux操作的任何组件都必须接受dispatch作为道具,以便能够进一步传递它。您不能再仅仅用connect()绑定操作创建者了,因为ShowNotificationWithTimeout()并不是真正的操作创建者。它不返回Redux操作。

此外,记住哪些函数是同步操作创建者(如ShowNotification())和哪些函数是异步帮助者(如ShowNotificationWithTimeout())可能会很困难。你必须不同地使用它们,并注意不要把它们误认为彼此。

这就是寻找一种方法来“合法化”这种模式的动机,这种模式向帮助器函数提供dispatch并帮助还原“see”这样的异步操作创建者,将其作为普通操作创建者的特例,而不是完全不同的函数。

如果你还在我们公司工作,并且你也认识到你的应用程序存在问题,欢迎你使用Redux Thunk中间件。

总之,Redux Thunk教Redux识别实际上是函数的特殊类型的动作:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk)
)

// It still recognizes plain object actions
store.dispatch({ type: 'INCREMENT' })

// But with thunk middleware, it also recognizes functions
store.dispatch(function (dispatch) {
  // ... which themselves may dispatch many times
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })

  setTimeout(() => {
    // ... even asynchronously!
    dispatch({ type: 'DECREMENT' })
  }, 1000)
})

启用此中间件时,如果您分派了一个函数,Redux Thunk中间件将给它dispatch作为参数。它也会“吞下”这样的动作,所以不要担心你的reducer接收到奇怪的函数参数。还原器将只接收简单的对象操作--或者直接发出,或者由我们刚才描述的函数发出。

这看起来不太有用,是吗?不是在这种特殊情况下。但是,它允许我们将ShowNotificationWithTimeout()声明为常规Redux操作创建者:

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

请注意,该函数与我们在上一节中编写的函数几乎完全相同。但是,它不接受dispatch作为第一个参数。相反,它返回一个接受dispatch作为第一个参数的函数。

我们如何在组件中使用它?当然,我们可以这样写:

// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)

我们调用async action creator来获取只需要dispatch的内部函数,然后传递dispatch

然而,这甚至比原来的版本更尴尬!我们为什么要走那条路?

因为我之前跟你说过的话。如果启用了Redux Thunk中间件,则当您试图分派函数而不是action对象时,中间件将以dispatch方法本身作为第一个参数调用该函数。

所以我们可以这样做:

// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))

最后,分派一个异步动作(实际上是一系列动作)看起来与同步分派单个动作到组件没有什么区别。这很好,因为组件不应该关心某些事情是同步发生还是异步发生。我们只是把它抽象化了。

注意,由于我们“教”Redux识别这样的“特殊”动作创建者(我们称它们为thunk动作创建者),我们现在可以在任何我们会使用常规动作创建者的地方使用它们。例如,我们可以将它们与connect()一起使用:

// actions.js

function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

// component.js

import { connect } from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)

通常,还原器包含用于确定下一个状态的业务逻辑。然而,减速机只有在动作被调度后才会起作用。如果在thunk操作创建者中有一个副作用(比如调用API),并且您希望在某种情况下防止它,该怎么办?

如果不使用thunk中间件,您只需在组件内部进行以下检查:

// component.js
if (this.props.areNotificationsEnabled) {
  showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}

然而,提取操作创建者的目的是将这种重复的逻辑集中在许多组件上。幸运的是,Redux Thunk为您提供了一种读取Redux存储当前状态的方法。除了dispatch之外,它还将getstate作为第二个参数传递给从thunk操作创建者返回的函数。这让thunk读取存储的当前状态。

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch, getState) {
    // Unlike in a regular action creator, we can exit early in a thunk
    // Redux doesn’t care about its return value (or lack of it)
    if (!getState().areNotificationsEnabled) {
      return
    }

    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

不要滥用这种模式。当有可用的缓存数据时,它对于退出API调用是很好的,但它不是构建业务逻辑的很好的基础。如果您只使用getState()来有条件地分派不同的操作,请考虑将业务逻辑放入缩减器中。

现在您已经对Thunk的工作方式有了基本的直觉,请查看使用它们的Redux async示例。

你可能会发现很多thunks回报承诺的例子。这不是必需的,但可以非常方便。Redux并不关心您从thunk返回什么,但它会从dispatch()中给出它的返回值。这就是为什么您可以通过调用dispatch(someThunkReturningPromise()))从thunk返回一个Promission并等待它完成的原因。然后(...)

您还可以将复杂的thunk操作创建者拆分为几个较小的thunk操作创建者。thunks提供的dispatch方法可以接受thunks本身,因此您可以递归地应用模式。同样,这与承诺一起工作最好,因为您可以在此基础上实现异步控制流。

对于某些应用程序,您可能会发现自己处于这样的情况:您的异步控制流需求过于复杂,无法用Thunks来表达。例如,重试失败的请求、使用令牌的重新授权流或分步登录,如果以这种方式编写,可能会过于冗长和容易出错。在这种情况下,您可能需要查看更高级的异步控制流解决方案,如Redux Saga或Redux Loop。对它们进行评估,比较与你的需求相关的例子,选出你最喜欢的一个。

最后,不要使用任何东西(包括thunks),如果你没有真正的需要。请记住,根据需求,您的解决方案看起来可能像

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

除非你知道你为什么要这么做,否则别担心。

 类似资料:
  • 问题内容: 我有一个操作可以更新应用程序的通知状态。通常,此通知将是错误或某种信息。然后,我需要在5秒钟后调度另一项操作,该操作会将通知状态恢复为初始状态,因此没有通知。其背后的主要原因是提供了5秒钟后通知自动消失的功能。 我没有使用并返回其他动作的运气,也找不到在线完成的方法。因此,欢迎提出任何建议。 问题答案: 不要陷入[认为图书馆应该规定如何做每件事的陷阱。如果您想在JavaScript中执

  • 解决方案(更新): 我认为任何操作都会导致react-redux-link调用mapState函数,但是当一个操作没有改变任何事情时,情况就不是这样了。 我有一个localStorage模块,它分派操作,但不更改状态,而是写入localStorage。该模块具有容器中使用的选择器,但在状态实际更改之前不会调用这些选择器,因此只有在调度另一个更改状态的操作后,UI才会正确显示。 问题 当我把商店放在

  • 我试图结合反应路由器v4,反应,和redux。因为react-router跟踪URL,所以我选择将该状态块排除在redux模型之外。 但我仍然需要一种方法来在react路由器发生路由更改时调度redux操作。哪里是最好的地方? 我的第一次尝试是将它放在react路由器链接的onClick属性中: 其思想是,当用户单击链接时,dispatchAction()将更新redux状态,然后加载相册组件。

  • 我有一个redux saga设置,工作正常。我的一个分派任务是创建一个新订单,然后一旦创建了订单,我就想用更新后的状态做一些事情。 由于 createOrder 操作触发调用 API 的重订传奇,因此存在延迟,因此在我的函数 do 之前不会更新此 .props.user 命令某些内容被调用。我可以设置一个超时,但这似乎不是一个可持续的想法。 我已经阅读了有关Stack Overflow的类似问题,

  • 问题内容: 我在redux存储中有一个Cart数组,其中包含我添加到购物车中的所有项目 像这样 并且我有一个输入来添加优惠券代码,当我添加优惠券代码会降低总价,所以如果我没有在商店中存储总计,该如何实现?并在添加此优惠券后渲染总价! 这是我的代码片段 reducer / index.js 购物车屏幕 问题答案: 我的建议是不要在状态中存储冗余(派生的)信息。这意味着您不需要(也不应该!)将总数存储

  • 我有与K8s集成的集群自动缩放器,工作节点从0缩放。我希望每个节点不调度超过2个豆荚。我在库贝莱特等级上设置了吊舱限制。当我用4个吊舱并行运行5个作业时,它限制扩展3个节点,但它试图只在2个节点和一个吊舱上调度,以减少到期限制。在K8S中是否有调度限制参数?只在特定时间(一些睡眠参数)后安排豆荚?当所有工人都准备好时,我们需要等待