前端开发中的乐观更新

事情的起因是这样的,在使用react开发项目的时候,邮箱规则列表页面有一个Switch按钮,点击按钮可以切换规则的启用或者禁用状态,刚开始的做法是:用户点击了按钮之后先改变IsEnabled的状态,然后调用update接口,更新服务器端的接口,使用await等到update接口调用成功之后,再去调用获取邮箱规则列表的接口,更新列表状态:

const handleEdit = async (flag: boolean, item: MailRuleItem) => {
        // 1. 创建更新后的对象(使用函数式更新确保最新状态)
        const updateItem = {
          ...item,
          IsEnabled: flag ? 1 : 0,
          Actions: {
            ...item.Actions,
            MoveToFolder: item.Actions.MoveToFolder?.UniqueId,
            Delete: item.Actions.Delete ? '1' : '0',
            MarkAsRead: item.Actions.MarkAsRead ? '1' : '0',
            CopyToFolder: item.Actions.CopyToFolder?.UniqueId,
          },
          Conditions: { ...item.Conditions }
        };
        await updatemailrule(updateItem);//更新的接口
       onReload();//刷新列表
      };

遇到的问题是:update接口响应比较慢,导致点击完按钮之后没有反应,好像是按钮没被点击一样,用户体验不好
需求:点击完之后,立马更新按钮的状态,如果接口发生错误,需要恢复按钮的状态
这里通过各种搜索,了解到了乐观更新,这个词听着很陌生,但其实很简单,原理就是点击完按钮之后,立马把被点击的数据的状态更改,给人的感觉是接口已经调用成功了,但实际仅仅是前端意义的更改,关于乐观更新解释如下:
乐观更新是一种提升用户体验的前端优化策略,其核心思想是:在等待服务器响应前,先假设操作会成功,立即更新本地UI状态。如果最终请求失败,再回滚到之前的状态。

核心特点

  1. 即时反馈

    • 用户操作后UI立即变化,无需等待网络延迟
    • 典型场景:点赞、开关切换、列表项操作
  2. 异步补偿

    • 请求失败时自动回滚状态
    • 通常会配合错误提示
  3. 状态可逆

    • 必须保留操作前的状态副本
    • 回滚时需要准确恢复上下文

实现原理(三步流程)

    用户->>UI: 触发操作(如点击开关)
    UI->>UI: 立即更新本地状态(乐观更新)
    UI->>服务端: 发送异步请求
    alt 请求成功
        服务端-->>UI: 返回200
        UI->>UI: 保持更新后状态(可选二次确认)
    else 请求失败
        服务端-->>UI: 返回错误
        UI->>UI: 自动回滚状态+错误提示
    end

典型实现代码(React示例)

const [items, setItems] = useState(data);

const handleToggle = async (id) => {
  // 1. 保存原始状态
  const originalItems = [...items];
  
  // 2. 乐观更新:立即切换UI状态
  setItems(prev => prev.map(item => 
    item.id === id ? { ...item, active: !item.active } : item
  ));

  try {
    // 3. 发送真实请求
    await api.toggleItem(id); 
  } catch (error) {
    // 4. 失败时回滚
    setItems(originalItems);
    toast.error("更新失败");
  }
};

适用场景 vs 不适用场景

适合场景 不适合场景
✅ 高频交互操作(点赞/收藏) ❌ 金融交易等关键操作
✅ 延迟敏感型功能(开关切换) ❌ 依赖服务端复杂计算的场景
✅ 幂等性操作(可重复执行) ❌ 非幂等性操作

最后使用乐观更新问题得以解决:

    const handleEdit = async (flag: boolean, item: MailRuleItem) => {
        // 1. 创建更新后的对象(使用函数式更新确保最新状态)
        const updateItem = {
          ...item,
          IsEnabled: flag ? 1 : 0,
          Actions: {
            ...item.Actions,
            MoveToFolder: item.Actions.MoveToFolder?.UniqueId,
            Delete: item.Actions.Delete ? '1' : '0',
            MarkAsRead: item.Actions.MarkAsRead ? '1' : '0',
            CopyToFolder: item.Actions.CopyToFolder?.UniqueId,
          },
          Conditions: { ...item.Conditions }
        };
      
        // 2. 乐观更新 - 使用函数式更新确保基于最新状态
        setRules(prevRules => {
          // 返回更新后的状态
          return prevRules.map(rule => {
            if (rule.Id === item.Id) {
              return updateItem;
            } else {
              return rule;
            }
          }
            
          );
        });
      
        try {
          // 3. 发送异步请求
          await updatemailrule(updateItem);
          // 4. 请求成功后,可以调用onReload获取最新数据
          // 或者直接使用返回的更新后数据(更推荐)
          onReload();
        } catch (error) {
          // 5. 请求失败时,回滚到之前保存的快照
          setRules(prevRules => {
            // 找到当前项的原始状态
            const originalItem = prevRules.find(r => r.id === item.id);
            if (!originalItem) return prevRules;
            return prevRules.map(rule => {
                if (rule.id === item.id) {
                  return originalItem;
                } else {
                  return rule;
                }
            }
              
            );
          });
          console.error("更新失败:", error);
        }
      };
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容