【问题标题】:How to avoid race conditions when fetching data with Redux?使用 Redux 获取数据时如何避免竞争条件?
【发布时间】:2016-10-10 19:59:35
【问题描述】:

我们有一个异步获取对象的操作,我们称它为getPostDetails,它接受获取哪个帖子的参数 一个身份证。用户会看到一个帖子列表,并且可以单击其中一个以获取一些详细信息。

如果用户点击“发布 #1”,我们会发送一个 GET_POST 操作,可能看起来像这样。

const getPostDetails = (id) => ({
  type: c.GET_POST_DETAILS,
  promise: (http) => http.get(`http://example.com/posts/#${id}`),
  returnKey: 'facebookData'
})

这是由一个中间件获取的,它向 Promise 添加一个成功处理程序,它将调用类似的操作 GET_POST__OK 带有反序列化的 JSON 对象。 reducer 看到这个对象并将其应用到 store。一个典型的 __OKreducer 长这样。

[c.GET_ALL__OK]: (state, response) => assign(state, {
  currentPost: response.postDetails
})

稍后我们有一个组件,它查看currentPost 并显示当前帖子的详细信息。

但是,我们有一个竞争条件。如果用户一个接一个地提交了两个GET_POST_DETAILS操作,则 不保证我们收到 __OK 操作的顺序,如果第二个 http 请求在第一个之前完成,则 状态将变得不正确。

    Action                   => Result
    ---------------------------------------------------------------------------------
|T| User Clicks Post #1      => GET_POST for #1 dispatched => Http Request #1 pending
|i| User Clicks Post #2      => GET_POST for #2 dispatched => Http Request #2 pending
|m| Http Request #2 Resolves => Results for #2 added to state
|e| Http Request #1 Resolves => Results for #1 added to state
 V

我们如何确保用户点击的最后一个项目总是优先?

【问题讨论】:

    标签: javascript redux


    【解决方案1】:

    Dan 的解决方案可能更好,但另一种解决方案是在第二个请求开始时中止第一个请求。

    你可以通过将你的动作创建者拆分成一个异步的来做到这一点,它可以从存储中读取并调度其他动作,redux-thunk 允许你这样做。

    您的异步操作创建者应该做的第一件事是检查存储中是否存在现有承诺,如果有则中止它。如果没有,它可以发出请求,并分派一个“请求开始”动作,其中包含承诺对象,该对象被存储以备下次使用。

    这样,只有最近创建的 Promise 才会解析。如果这样做,您可以使用接收到的数据发送成功操作。如果承诺因某种原因被拒绝,您也可以发送错误操作。

    【讨论】:

    • 请注意,我们不建议将 Promise 存储在 state 中。理想情况下,它应该是可序列化的。但是,我确实描述了您在 this lesson 中建议的方法。这是一个很好的方法,但不能取代规范化状态。 (结合这两种方法是个好主意。)
    • 这个解决方案在我看来并不像它应该做的那样。当第二个请求进来时,您想中止第一个请求。然而,store 中的 promise 似乎起到了保护作用,防止在运行时发出任何其他请求:“你的异步操作创建者应该做的第一件事是检查 store 中是否存在现有的 promise,如果存在则中止它是一个。'
    • @Richardinho “如果有就中止它”,我的意思是中止现有的请求,而不是中止正在发出的新请求。这更有意义吗?
    • 是的,听起来您确实在说,如果存在现有请求,那么您只需中止并且不执行任何其他操作。
    【解决方案2】:

    问题是由于次优的状态组织造成的。

    在 Redux 应用程序中,像 currentPost 这样的状态键通常是一种反模式。如果您每次导航到另一个页面时都必须“重置”状态,那么您将丢失 main benefits of Redux (or Flux): 缓存之一。例如,如果任何导航重置状态并重新获取数据,您将无法立即返回。

    存储此信息的更好方法是将postsByIdcurrentPostId 分开:

    {
      currentPostId: 1,
      postsById: {
        1: { ... },
        2: { ... },
        3: { ... }
      }
    }
    

    现在您可以同时获取任意数量的帖子,并将它们独立合并到postsById 缓存中,而不必担心获取的帖子是否是当前帖子。

    在您的组件中,您总是会读取state.postsById[state.currentPostId],或者更好的是,从reducer 文件中导出getCurrentPost(state) 选择器,这样组件就不会依赖于特定的状态形状。

    现在没有竞争条件了并且你有一个帖子缓存,所以当你回去时不需要重新获取。稍后,如果您希望从 URL 栏控制当前帖子,您可以将 currentPostId 从 Redux 状态中完全删除,而是从您的路由器中读取它——其余逻辑将保持不变。


    虽然这并不完全一样,但我碰巧有另一个具有类似问题的示例。查看code beforecode after。这与您的问题完全不一样,但希望它显示了状态组织如何帮助避免竞争条件和不一致的道具。

    我还录制了一个解释这些主题的免费视频系列,因此您可能想要check it out

    【讨论】:

    • 我的情况有点不同。如果我也有currentPostIdpostById,我该怎么办,但我必须同时获取所有帖子的数据(创建时间、预览图片、元数据等),然后只获取一个选定帖子的内容,然后在postById中将此内容与此帖子的所有其他内容一起缓存?
    • @DanAbramov 嗨,我知道这是一个很老的问题,但是由于任何动作创建者都无法访问状态,动作创建者如何决定它是被缓存还是需要获取远程对象?
    • 如果currentListOfSearchResults 是一个数组,而不仅仅是一个currentPost,该怎么办?我通常最终会跳过 Redux 来获取这些结果,并将它们置于我的页面组件的本地状态。
    • 我很欣赏这个解决方案。但是,我无法很好地解释在以下情况下要做什么: 1. 您有可以动态创建的父级。 2. 每个父节点有 5 个子节点,它们都发出 fetch 请求。这些孩子每个人都有自己的减速器来促进这一点。 3. 如果我在应用程序中创建一个新的父级,我要么需要创建新的子级减速器,要么取消以前活动父级的所有飞行请求并为当前活动的父级发起新请求。有人遇到过类似的情况吗?我已经阅读并尝试了 Dan 的代码拆分回复,但不起作用。
    • @Dan Abramov,这很棒。我有一个关于这种模式的问题。我们多久从存储中清除状态?
    猜你喜欢
    • 2020-08-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多