乐观更新

    1797
    最后修改于

    本文旨在介绍

    乐观更新,react query 和 zustand/jotai

    reactquery 可以很好的做到一次性乐观更新,整个流程就是 mutate->success/error->settled
    mutate,开始更新乐观更新,success 之后什么也不做,error 就将乐观更新的内容进行回退,最后 settled 重新获取数据

    这里有几个问题

    对于乐观更新的这个数据,怎么展示。

    1. 像其他正常数据一样展示。
    2. 展示一个请求的过程 (半乐观)
      对于原有的分页数据,怎么跟乐观更新搭配

    对于一次乐观更新,有几种状态。
    乐观更新 -> 成功
    乐观更新 -> 失败
    失败的乐观更新 -> 重试乐观更新

    交互上有几种策略

    1. 直接乐观更新,作为正常数据对待。失败就回退,不提示用户
    2. 乐观更新,展示正常数据,失败回退,提示用户
    3. 乐观更新,展示为发送中的数据,失败回退,提示用户
    4. 展示为发送中的数据,失败不回退,而是提示用户重新发送。
    5. 初次乐观更新视为正常数据。发送失败不回退,进行提示,用户可以交互重试,重试的时候展示发送状态。
      失败回到上一流程,成功则更新

    最复杂的,用户体验也最好的可能就是第五种,这也是微信 / QQ 这类 IM 使用的策略

    以上是对一个 list 中新增项,除此之外,还有对某项的更新对 list 中某一项进行更改,还有对 list 中内容的删除

    这时候,在客户端状态中,有几类数据

    1. 来自于服务器的有效数据
    2. 从客户端发送的单个数据
      2.1 发送中
      2.2 发送成功
      2.3 发送失败
    3. 从客户端并发发送的多个数据。
      客户端数据相比于服务端数据有些不同,比如缺少 id,多了一个状态标识。

    但是缺少的 id 又不能乱给,因为更新成功后,服务端数据如果要重新获取,我们希望,服务端新增的数据能够准确替换乐观更新填充的数据

    在客户端可能还需要对拿到的 list 内容进行一遍处理,与客户端的内容进行区分。

    如果显示要进行标识。

    回到上面的问题,以 react-query 介绍的方案为例

    1. 通过 ui,通过 mutate 的 pending 状态,结合传入 mutate 的参数,进行更新,然后重新获取数据,这是上面第一种策略
    2. 通过 ui,通过 mutate 的 variable,iserror,加上错误提示,允许重试

    以上这两种都出现在简单局部情况下,对于数据消费组件,除了要感知 query,还必须要感知到 mutation,即使 mutation 跟组件不在一起,还是需要通过 mutationstate 获取 mutation 的状态

    1. 更新 cache,在 mutate 内部,onmutate 的时候先乐观更新数据,setquerydata
      然后结束按需对乐观更新到数据操作,最后重新验证数据。
      这个流程对于单个数据很不错
      但是如果要展示发送中 / 发送失败,然后进行重试。在重试开始前,如果 revalidate 了数据,那么失败的内容会在 cache 中丢失。因此需要单独的客户端状态管理。

    所以 react query 的文档方案不够用

    设想要实现第五种方案。那么最终现实在屏幕上的。
    有几种数据

    1. 服务器数据
    2. 客户端还未发送到服务端的数据
    3. 客户端已 / 正在发送的数据,正在到达客户端。

    1.2. 没有重叠,但是是都与 3 有交叠。

    整个数据流,从 2 开始,进入 3,然后 1 被更新。

    这时候,3,1. 有交叠 <已发送,服务端更新的数据>

    如果失败,3 中数据要回到 2
    如果成功,3 中数据会和 1 有重叠

    但是给到 list 的,应当是统一的,这有利于简化 ui 实现。否则数据在 2,3,1 的流转过程中,会造成 ui 上闪烁。

    因此一个每个数据都应该是 type=<id,data,status>,但是客户端 id 怎么做?
    这里隐含的一个问题是,当服务端更新数据之后,如何去重新增的服务端数据和发送成功的客户端数据。

    因为对 item 进行更新时,这个过程,有一个响应机会,可以很好的建立客户端数据和服务端数据的关联。,因此对于一个 insert rpc,不应该只返回 ok 信息,而应该连带返回一些有区分度先重要信息,比如 id

    这里是对 list 新增场景下的描述

    那么,list 中某一项的更改怎么做?在这里,客户端盒服务端的数量是一致的,都有唯一 id,但是仍然有状态,updating 可能成功,也可能失败。这里可以通过 setquerydata,更新某一项内容,及其状态,通过 cache 进行处理。缺点在于仍不可轻易重试 (需要通过 mutation variable),判断 error

    因此需要在客户端保存一个副本,指示 id,和目标状态以及当前状态。然后使用渲染时,每一项查询自己是否有待更新副本,根据这个数据进行重试,这里对 list 的修改其实本质上是对 item 到修改

    然后就是 delete 的成功 / 失败,怎么做?客户端需要保有 deleting 的状态,id,status,先乐观更新,从 cache 中移除,成功删除状态标识,失败就将缓存恢复。最后 invalidquery

    以上,都是非分页情况下,cud 的乐观更新

    适用多组件复用,但本身数据量不大 (不分页)

    单组件使用直接用 react 的 useOptimistic 也可以。(不共享状态)

    // 竞态条件可以通过 mutate 时取消 query 进行

    最后,分页情况下。如何处理?

    比如,在页面末尾加上元素,筛选机制。
    1. 新增数据位于分页数据的顶部
    2. 新增数据位于分页数据的底部 / 中间。
    3. 因为筛选规则的存在,新增数据不属于任何一页

    分页的展示,无限列表,那么展示上乐观更新的数据不会有问题,新增 / 删除 / 更新

    还有一种,在页面上展示分页数据,展示分页逻辑,那么乐观更新就会导致 page 的变动,从交互上,分页改变了 page,pagesize,这会很复杂

    在无限列表中,分页的数据还有可能要处理虚拟列表以缓解性能压力

    先看无限列表。
    查询上,分页拆分到不同的请求上。

    对于 reactquery,统一缓存在一个 useInfiniteQuery 中,然后用 page 去 map 到每一页

    无限列表给出的 api 包括,load,has,isLoading 某个 page。
    而 has 的判断需要由请求结果的 total,page 进行判断

    • 🥳0
    • 👍0
    • 💩0
    • 🤩0
    总浏览量 4,263最近访客来自 US