如何使用超时分派Redux动作?

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

我没有使用setTimeout并返回另一个操作的运气,并且无法找到这是如何在线完成的。 所以任何建议都是值得欢迎


不要陷入图书馆应该规定如何去做所有事情的陷阱。 如果你想在JavaScript中使用超时,你需要使用setTimeout 。 没有理由为什么Redux行动应该有任何不同。

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)

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

提取Async Action Creator

上述方法在简单情况下可以正常工作,但您可能会发现它存在一些问题:

  • 它迫使你在想要显示通知的任何位置复制这个逻辑。
  • 这些通知没有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 interval ID and call
      // clearInterval(), 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.')    
    

    这解决了逻辑重复的问题,并将我们从竞争状况中解救出来。

    Thunk中间件

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

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

    例如,我们必须通过dispatch ,这似乎是不幸的。 这使得分离容器和表示组件变得更加棘手,因为以上述方式异步分派Redux动作的组件必须接受dispatch作为prop,以便它可以进一步传递它。 你不能仅仅使用connect()绑定动作创建者,因为showNotificationWithTimeout()并不是一个真正的动作创建者。 它不会返回Redux操作。

    此外,记住哪些函数是同步动作创建者(如showNotification()和哪些是异步助手(如showNotificationWithTimeout()会很尴尬。 你必须以不同的方式使用它们,并小心不要将它们彼此误认。

    这是找到一种方法来将这种dispatch提供给帮助函数的方法“合法化”,并帮助Redux“将”这种异步操作创建者视为正常操作创建者的特例,而不是完全不同的功能。

    如果您仍然与我们在一起,并且您也认识到您的应用存在问题,欢迎使用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 。 它也会“吞下”这样的动作,所以不用担心你的减速器会收到奇怪的函数参数。 您的简化器只会接收简单的对象操作 - 要么直接发送,要么由我们刚才描述的函数发出。

    这看起来不是很有用,是吗? 不是在这种特殊情况下。 不过,它让我们将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)
    

    我们正在调用异步动作创建器来获取需要dispatch的内部函数,然后我们通过dispatch

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

    因为我之前告诉你的。 如果Redux Thunk中间件已启用,则只要尝试分派函数而不是操作对象,中间件就会使用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)
    

    在Thunks读书状态

    通常你的reducer包含确定下一个状态的业务逻辑。 但是,减速器只在执行动作后才会执行。 如果您在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()来有条件地分派不同的操作,请考虑将业务逻辑放入reducer中。

    下一步

    现在您已经对thunk的工作原理有了一个基本的认识,请查看使用它们的Redux异步示例。

    你可能会发现许多例子,其中thunk返回Promises。 这不是必需的,但可以非常方便。 Redux不关心你从thunk返回的内容,但是它会从dispatch()返回它的返回值。 这就是为什么你可以从一个thunk中返回一个Promise,并通过调用dispatch(someThunkReturningPromise()).then(...)等待它完成dispatch(someThunkReturningPromise()).then(...)

    您也可以将复杂的thunk动作创作者分成几个较小的thunk动作创作者。 thunk提供的dispatch方法可以接受thunk本身,所以你可以递归地应用这个模式。 同样,这对于Promise来说效果最好,因为您可以在其上实现异步控制流。

    对于某些应用程序,您可能会发现自己处于异步控制流程要求过于复杂而无法用粗体表示的情况。 例如,重试失败的请求,使用令牌重新授权流程,或者一步一步地上传可能过于冗长且容易出错。 在这种情况下,您可能需要查看更高级的异步控制流解决方案,如Redux Saga或Redux Loop。 评估他们,比较与你的需求相关的例子,并挑选你最喜欢的。

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

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

    除非你知道你为什么要这样做,否则不要冒汗。


    使用Redux-saga

    正如丹·阿布拉莫夫所说,如果你想对你的异步代码进行更高级的控制,你可以看看REDX-SAGA。

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

    总的来说,Redux-saga提供了一个ES6生成器解释器,允许您轻松编写看起来像同步代码的异步代码(这就是为什么您经常会在Redux-saga中发现无限循环的原因)。 不知何故,Redux-saga直接在Javascript中构建自己的语言。 Redux-saga起初可能感觉有点困难,因为你需要对生成器有基本的了解,但也要了解Redux-saga提供的语言。

    我将在这里尝试描述我在redux-saga之上构建的通知系统。 这个例子目前在生产中运行。

    先进的通知系统规范

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

    我的制作应用程序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_REQUESTED事件。 如果您发送4个请求,则只会显示3个通知,并且第一个通知消失后第四个通知会稍后显示。

    请注意,我并不特别建议从JSX调度TOAST_DISPLAY_REQUESTED 。 您宁愿添加另一个侦听您已经存在的应用事件的传奇,然后派发TOAST_DISPLAY_REQUESTED :您的触发通知的组件,不必与通知系统紧密耦合。

    结论

    我的代码并不完美,但在几个月的时间里运行在生产中,并且有0个bug。 Redux-saga和生成器最初有点困难,但一旦你了解它们,这种系统就很容易建立。

    实现更复杂的规则很容易,例如:

  • 当太多的通知“排队”时,为每个通知提供较少的显示时间,以便队列大小可以更快降低。
  • 检测窗口大小更改并相应地更改显示的最大通知数(例如,desktop = 3,phone portrait = 2,phone landscape = 1)
  • 很高兴,祝你好运,用thunk可以很好地实施这种东西。

    请注意,您可以使用与redux-saga非常相似的redux-observable来完成同样的事情。 它几乎是一样的,并且是发电机和RxJS之间味道的问题。


    你可以用redux-thunk来做到这一点。 在redux文档中有一个像setTimeout那样的异步操作指南。

    链接地址: http://www.djcxy.com/p/59047.html

    上一篇: How to dispatch a Redux action with a timeout?

    下一篇: Do loops check the array.length every time when comparing i against array.length?