关于javascript:如何使用超时调度Redux操作?

How to dispatch a Redux action with a timeout?

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

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


不要陷入认为图书馆应该规定如何做每件事的陷阱。如果您想在javascript中处理超时问题,则需要使用setTimeout。Redux操作没有任何不同的原因。好的。

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

这是迄今为止最简单的方法。这里没有具体的简化方法。好的。

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

同样,从连接的组件内部:好的。

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

唯一的区别是,在一个连接的组件中,您通常无法访问存储本身,而是将dispatch()或特定的动作创建者作为道具注入。不过,这对我们没有任何影响。好的。

如果您不喜欢在从不同的组件调度相同的操作时输入错误,那么您可能希望提取操作创建者而不是内联调度操作对象:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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()绑定过它们:好的。

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

到目前为止,我们还没有使用任何中间件或其他高级概念。好的。正在提取异步操作创建者

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

  • 它强制您在任何想要显示通知的地方复制这个逻辑。
  • 通知没有ID,因此如果您足够快地显示两个通知,您将具有竞争条件。当第一个超时结束时,它将发送HIDE_NOTIFICATION,错误地在超时之后提前隐藏第二个通知。

要解决这些问题,您需要提取一个集中超时逻辑并分派这两个操作的函数。可能是这样的:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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,而无需重复此逻辑或具有带有不同通知的竞态条件:好的。

1
2
3
4
5
// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

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

为什么showNotificationWithTimeout()接受dispatch作为第一个论点?因为它需要向商店发送操作。通常情况下,一个组件可以访问dispatch,但是由于我们希望外部功能控制调度,所以我们需要让它控制调度。好的。

如果您有一个从某个模块中导出的单件商店,您可以直接导入它和dispatch,而不是直接导入它:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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.')

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

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

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

返回到以前的版本:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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作为一个道具,以便它能够进一步传递它。你不能再把动作创建者和connect()绑定在一起了,因为showNotificationWithTimeout()不是真正的动作创建者。它不返回redux操作。好的。

此外,还很难记住哪些函数是同步动作创建者(如showNotification())和异步帮助者(如showNotificationWithTimeout())。你必须以不同的方式使用它们,注意不要把它们错认为是彼此的。好的。

这就是找到一种"使"这种向辅助函数提供dispatch的模式合法化的原因,并帮助减少将这种异步动作创建者"视"为正常动作创建者的特殊情况,而不是完全不同的功能。好的。

如果您仍然和我们在一起,并且您也认识到应用程序中存在问题,欢迎您使用ReduxThunk中间件。好的。

在一个要点中,redux thunk教redux认识到实际上是函数的特殊类型的动作:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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操作创建者:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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作为第一个参数的函数。好的。

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

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

我们调用异步操作创建者来获取只需要dispatch的内部函数,然后传递dispatch。好的。

不过,这比原来的版本更尴尬!我们为什么要走那条路?好的。

因为我以前告诉过你的。如果启用了redux thunk中间件,任何时候您试图调度一个函数而不是一个操作对象,中间件都会以dispatch方法本身作为第一个参数调用该函数。好的。

所以我们可以这样做:好的。

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 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中间件,只需在组件内部进行检查:好的。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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()有条件地分派不同的操作,那么考虑将业务逻辑放入Reducers中。好的。下一步

既然您对thunk是如何工作有了基本的直觉,那么请查看使用它们的redux异步示例。好的。

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

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

对于某些应用程序,您可能会发现您的异步控制流需求过于复杂,无法用thunk表示。例如,当以这种方式写入时,重试失败的请求、使用令牌的重新授权流或逐步启动可能过于冗长且容易出错。在这种情况下,您可能希望了解更高级的异步控制流解决方案,如Redux SAGA或Redux循环。对它们进行评估,比较与您的需求相关的例子,然后选择您最喜欢的一个。好的。

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

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

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


使用Redux Saga

正如DanAbramov所说,如果您想要对异步代码进行更高级的控制,您可以看看ReduxSaga。

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

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

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

高级通知系统规范

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

结果

我的生产应用程序stample.co的屏幕截图

toasts

代码

这里我将通知命名为toast,但这是命名细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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)
    ]
}

减速器:

1
2
3
4
5
6
7
8
9
10
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个通知,第4个通知将在第一个通知消失后稍晚出现。

注意,我并不特别推荐从JSX发送TOAST_DISPLAY_REQUESTED。您更愿意添加另一个SAGA来监听已经存在的应用程序事件,然后发送TOAST_DISPLAY_REQUESTED:触发通知的组件不必与通知系统紧密耦合。

结论

我的代码并不完美,但在生产中运行了几个月,其中有0个bug。ReduxSaga和Generator最初有点困难,但一旦你理解了它们,这种系统就很容易构建。

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

  • 当太多的通知被"排队"时,为每个通知提供较少的显示时间,以便队列大小可以更快地减小。
  • 检测窗口大小的变化,并相应地更改显示的最大通知数(例如,桌面=3,电话纵向=2,电话横向=1)

诚然,祝你好运,用thunks正确地实现这类工作。

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


我还建议您看看SAM模式。

SAM模式主张包括"下一步动作谓词",其中(自动)动作,如"通知在5秒后自动消失"在模型更新后触发(SAM模型~ Reducer状态+存储)。

模式提倡一次一个动作排序和模型突变,因为模型"控制"的"控制状态"控制下一个动作谓词启用和/或自动执行的动作。您无法预测(一般而言)系统在处理操作之前的状态,因此您的下一个预期操作是否被允许/可能。

例如代码,

1
2
3
4
5
6
7
8
export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

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

不允许与sam一起使用,因为可以调度hidenotification操作的事实取决于成功接受值"shownotification:true"的模型。模型的其他部分可能会阻止它接受它,因此,没有理由触发hidenotification操作。

我强烈建议在存储更新之后实现适当的下一步动作谓词,并且可以知道模型的新控制状态。这是实现您正在寻找的行为的最安全的方法。

如果你愿意的话,你可以在吉特加入我们。这里还有一个山姆入门指南。


你可以用redux thunk来实现。Redux文档中有一个关于异步操作(如setTimeout)的指南。


包含示例项目的存储库

目前有四个示例项目:

  • 以内联方式写入异步代码
  • 正在提取异步操作创建者
  • 使用废液
  • 使用Redux Saga
  • 公认的答案太棒了。

    但有些东西不见了:

  • 没有可运行的示例项目,只是一些代码片段。
  • 其他备选方案没有样本代码,例如:
  • 复仇传奇
  • 因此,我创建了hello async存储库来添加缺少的内容:

  • 可运行的项目。您可以下载并运行它们而不需要修改。
  • 为更多备选方案提供示例代码:
    • 复仇传奇
    • 回流环
  • 复仇传奇

    接受的答案已经为异步代码内联、异步操作生成器和redux thunk提供了示例代码段。为了完整起见,我提供了redux saga的代码片段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // actions.js

    export const showNotification = (id, text) => {
      return { type: 'SHOW_NOTIFICATION', id, text }
    }

    export const hideNotification = (id) => {
      return { type: 'HIDE_NOTIFICATION', id }
    }

    export const showNotificationWithTimeout = (text) => {
      return { type: 'SHOW_NOTIFICATION_WITH_TIMEOUT', text }
    }

    行动是简单而纯粹的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // component.js

    import { connect } from 'react-redux'

    // ...

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

    // ...

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

    组件没有特殊之处。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // sagas.js

    import { takeEvery, delay } from 'redux-saga'
    import { put } from 'redux-saga/effects'
    import { showNotification, hideNotification } from './actions'

    // Worker saga
    let nextNotificationId = 0
    function* showNotificationWithTimeout (action) {
      const id = nextNotificationId++
      yield put(showNotification(id, action.text))
      yield delay(5000)
      yield put(hideNotification(id))
    }

    // Watcher saga, will invoke worker saga above upon action 'SHOW_NOTIFICATION_WITH_TIMEOUT'
    function* notificationSaga () {
      yield takeEvery('SHOW_NOTIFICATION_WITH_TIMEOUT', showNotificationWithTimeout)
    }

    export default notificationSaga

    Sagas基于ES6发电机

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // index.js

    import createSagaMiddleware from 'redux-saga'
    import saga from './sagas'

    const sagaMiddleware = createSagaMiddleware()

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

    sagaMiddleware.run(saga)

    与Redux Thunk相比赞成的意见

    • 你不会死在地狱里。
    • 您可以轻松地测试异步流。
    • 你的行为保持纯洁。

    欺骗

    • 这取决于相对较新的ES6发电机。

    如果上面的代码片段不能回答所有问题,请参考可运行项目。


    在尝试了各种流行的方法(动作创建者、thunk、sagas、epics、效果、定制中间件)之后,我仍然觉得可能还有改进的空间,所以我在这篇博客文章中记录了我的旅程,我应该把我的业务逻辑放在react/redux应用程序的哪里?

    就像这里的讨论一样,我试图对比和比较各种方法。最终,它引导我引入了一个新的库redux逻辑,它从epics、sagas和定制中间件中获得灵感。

    它允许您截取操作来验证、验证、授权以及提供执行异步IO的方法。

    一些常见的功能可以简单地声明为去块、限制、取消,并且只使用来自最新请求的响应(takeLatest)。Redux逻辑包装了为您提供此功能的代码。

    这使您可以自由地实现您喜欢的核心业务逻辑。除非你愿意,否则你不必使用观测仪或发电机。使用函数和回调、承诺、异步函数(异步/等待)等。

    执行简单5S通知的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const notificationHide = createLogic({
      // the action type that will trigger this logic
      type: 'NOTIFICATION_DISPLAY',
     
      // your business logic can be applied in several
      // execution hooks: validate, transform, process
      // We are defining our code in the process hook below
      // so it runs after the action hit reducers, hide 5s later
      process({ getState, action }, dispatch) {
        setTimeout(() => {
          dispatch({ type: 'NOTIFICATION_CLEAR' });
        }, 5000);
      }
    });

    我的repo中有一个更高级的通知示例,其工作原理类似于Sebastian Lorber所描述的,您可以将显示限制为n个项目,并在排队的任何项目中旋转。Redux逻辑通知示例

    我有各种各样的Redux Logic JSfiddle实时示例以及完整的示例。我将继续研究文档和示例。

    我很想听听你的反馈。


    我知道这个问题有点老,但我将介绍另一个使用Redux Observable aka的解决方案。史诗。

    引用官方文件:

    什么是Redux Observable?

    RxJS 5-based middleware for Redux. Compose and cancel async actions to
    create side effects and more.

    史诗是还原可观测的核心原语。

    It is a function which takes a stream of actions and returns a stream
    of actions. Actions in, actions out.

    或多或少,您可以创建一个函数,它通过流接收操作,然后返回一个新的操作流(使用常见的副作用,如超时、延迟、间隔和请求)。

    让我把代码贴出来,然后再解释一下

    JS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    import {createStore, applyMiddleware} from 'redux'
    import {createEpicMiddleware} from 'redux-observable'
    import {Observable} from 'rxjs'
    const NEW_NOTIFICATION = 'NEW_NOTIFICATION'
    const QUIT_NOTIFICATION = 'QUIT_NOTIFICATION'
    const NOTIFICATION_TIMEOUT = 2000

    const initialState = ''
    const rootReducer = (state = initialState, action) => {
      const {type, message} = action
      console.log(type)
      switch(type) {
        case NEW_NOTIFICATION:
          return message
        break
        case QUIT_NOTIFICATION:
          return initialState
        break
      }

      return state
    }

    const rootEpic = (action$) => {
      const incoming = action$.ofType(NEW_NOTIFICATION)
      const outgoing = incoming.switchMap((action) => {
        return Observable.of(quitNotification())
          .delay(NOTIFICATION_TIMEOUT)
          //.takeUntil(action$.ofType(NEW_NOTIFICATION))
      });

      return outgoing;
    }

    export function newNotification(message) {
      return ({type: NEW_NOTIFICATION, message})
    }
    export function quitNotification(message) {
      return ({type: QUIT_NOTIFICATION, message});
    }

    export const configureStore = () => createStore(
      rootReducer,
      applyMiddleware(createEpicMiddleware(rootEpic))
    )

    索引文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    import {configureStore} from './store.js'
    import {Provider} from 'react-redux'

    const store = configureStore()

    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    );

    App.JS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    import React, { Component } from 'react';
    import {connect} from 'react-redux'
    import {newNotification} from './store.js'

    class App extends Component {

      render() {
        return (
         
            {this.props.notificationExistance ? (<p>
    {this.props.notificationMessage}
    </p>) : ''}
            <button onClick={this.props.onNotificationRequest}>Click!</button>
         
        );
      }
    }

    const mapStateToProps = (state) => {
      return {
        notificationExistance : state.length > 0,
        notificationMessage : state
      }
    }

    const mapDispatchToProps = (dispatch) => {
      return {
        onNotificationRequest: () => dispatch(newNotification(new Date().toDateString()))
      }
    }

    export default connect(mapStateToProps, mapDispatchToProps)(App)

    解决这个问题的关键代码就像饼图一样简单,唯一不同于其他答案的是函数rootepic。

    要点1。与Sagas一样,您必须组合epics才能获得接收操作流并返回操作流的顶级函数,因此您可以将其与中间件工厂createpicmiddleware一起使用。在我们的情况下,我们只需要一个,所以我们只有我们的rootepic,所以我们不需要结合任何东西,但这是一个很好的了解事实。

    要点2。我们关注副作用逻辑的rootepic只需要5行代码,这太棒了!包括这个事实,这是非常声明性的!

    要点3。逐行rootepic解释(在注释中)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const rootEpic = (action$) => {
      // sets the incoming constant as a stream
      // of actions with  type NEW_NOTIFICATION
      const incoming = action$.ofType(NEW_NOTIFICATION)
      // Merges the"incoming" stream with the stream resulting for each call
      // This functionality is similar to flatMap (or Promise.all in some way)
      // It creates a new stream with the values of incoming and
      // the resulting values of the stream generated by the function passed
      // but it stops the merge when incoming gets a new value SO!,
      // in result: no quitNotification action is set in the resulting stream
      // in case there is a new alert
      const outgoing = incoming.switchMap((action) => {
        // creates of observable with the value passed
        // (a stream with only one node)
        return Observable.of(quitNotification())
          // it waits before sending the nodes
          // from the Observable.of(...) statement
          .delay(NOTIFICATION_TIMEOUT)
      });
      // we return the resulting stream
      return outgoing;
    }

    希望有帮助!


    为什么这么难?这只是用户界面逻辑。使用专用操作设置通知数据:

    1
    dispatch({ notificationData: { message: 'message', expire: +new Date() + 5*1000 } })

    以及显示它的专用组件:

    1
    2
    3
    4
    5
    const Notifications = ({ notificationData }) => {
        if(notificationData.expire > this.state.currentTime) {
          return {notificationData.message}
        } else return null;
    }

    在这种情况下,问题应该是"如何清理旧状态?","如何通知组件时间已更改"

    您可以实现一些超时操作,这些操作在组件的setTimeout上调度。

    也许每次显示新通知时都可以清除它。

    不管怎么说,应该在某个地方有一些setTimeout,对吧?为什么不在组件中做呢

    1
    2
    setTimeout(() => this.setState({ currentTime: +new Date()}),
               this.props.notificationData.expire-(+new Date()) )

    其动机是"通知淡出"功能实际上是一个用户界面问题。因此,它简化了对业务逻辑的测试。

    测试它是如何实现的似乎没有意义。只有验证通知何时应该超时才有意义。因此,代码到存根更少,测试更快,代码更干净。


    如果您希望对选择性操作进行超时处理,可以尝试使用中间件方法。我在有选择地处理基于承诺的行为时遇到了类似的问题,这个解决方案更加灵活。

    假设您的动作创建者如下所示:

    1
    2
    3
    4
    5
    //action creator
    buildAction = (actionData) => ({
        ...actionData,
        timeout: 500
    })

    超时可以在上述操作中保存多个值

    • 以毫秒为单位的数字-特定超时持续时间
    • 真-持续的超时时间。(在中间件中处理)
    • 未定义-用于立即调度

    您的中间件实现如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //timeoutMiddleware.js
    const timeoutMiddleware = store => next => action => {

      //If your action doesn't have any timeout attribute, fallback to the default handler
      if(!action.timeout) {
        return next (action)
      }

      const defaultTimeoutDuration = 1000;
      const timeoutDuration = Number.isInteger(action.timeout) ? action.timeout || defaultTimeoutDuration;

    //timeout here is called based on the duration defined in the action.
      setTimeout(() => {
        next (action)
      }, timeoutDuration)
    }

    现在,您可以使用redux将所有操作路由到此中间件层。

    1
    createStore(reducer, applyMiddleware(timeoutMiddleware))

    你可以在这里找到一些类似的例子


    正确的方法是使用redux thunk,它是Redux的流行中间件,根据Redux Thunk文档:

    "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".

    所以基本上它返回一个函数,并且您可以延迟调度或者将其置于条件状态。

    所以像这样的事情会为你做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import ReduxThunk from 'redux-thunk';

    const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

    function increment() {
      return {
        type: INCREMENT_COUNTER
      };
    }

    function incrementAsync() {
      return dispatch => {
        setTimeout(() => {
          // Yay! Can invoke sync or async actions with `dispatch`
          dispatch(increment());
        }, 5000);
      };
    }

    这很简单。使用trim redux包,在componentDidMount或其他地方这样写,然后在componentWillUnmount中杀死它。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    componentDidMount() {
      this.tm = setTimeout(function() {
        setStore({ age: 20 });
      }, 3000);
    }

    componentWillUnmount() {
      clearTimeout(this.tm);
    }


    redux本身是一个非常冗长的库,对于这种东西,您必须使用redux thunk之类的东西,它将提供dispatch函数,因此您可以在几秒钟后调度关闭通知。

    我创建了一个库来解决诸如冗长性和可组合性等问题,您的示例如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import { createTile, createSyncTile } from 'redux-tiles';
    import { sleep } from 'delounce';

    const notifications = createSyncTile({
      type: ['ui', 'notifications'],
      fn: ({ params }) => params.data,
      // to have only one tile for all notifications
      nesting: ({ type }) => [type],
    });

    const notificationsManager = createTile({
      type: ['ui', 'notificationManager'],
      fn: ({ params, dispatch, actions }) => {
        dispatch(actions.ui.notifications({ type: params.type, data: params.data }));
        await sleep(params.timeout || 5000);
        dispatch(actions.ui.notifications({ type: params.type, data: null }));
        return { closed: true };
      },
      nesting: ({ type }) => [type],
    });

    因此,我们编写同步操作来显示异步操作中的通知,这可以请求一些后台信息,或者稍后检查通知是否是手动关闭的。