解决前端竞态问题的乐观锁机制

1,259 阅读2分钟

在前端开发中,搜索框的实时搜索功能是一个常见场景。用户输入文字时,每输入一个字符就发起搜索请求。但这里存在一个潜在的竞态问题:如果后发起的请求比先发起的请求更快返回,可能会导致错误的搜索结果显示。
防抖能一定程度减少竞态问题的出现,但是无法杜绝。

竞态问题示例

假设用户快速输入"ab":

  1. 输入"a"时发起请求A
  2. 输入"b"时发起请求B
  3. 请求B先返回,显示"ab"的结果
  4. 请求A后返回,错误地覆盖为"a"的结果

乐观锁解决方案

我们可以借鉴后端乐观锁的机制来解决这个问题:

复现竞态问题示例

加锁解决竞态问题示例

架构图

image.png

源码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>竞态问题搜索示例</title>
    <style>
      #results {
        margin-top: 20px;
        border: 1px solid #ccc;
        padding: 10px;
        min-height: 100px;
      }
    </style>
  </head>
  <body>
    <h1>竞态问题搜索示例</h1>
    <input type="text" id="searchInput" placeholder="输入搜索内容..." />
    <div id="results">搜索结果将显示在这里</div>

    <script>
      // 全局请求版本号
      let requestVersion = 0

      // 模拟API请求
      async function mockSearchApi(query) {
        // 模拟网络延迟:查询越长,响应越快
        const delay = 1000 - query.length * 200
        await new Promise((resolve) => setTimeout(resolve, delay))
        return `查询"${query}"的结果,延迟${delay}ms`
      }

      // 防竞态的搜索函数
      async function raceSafeSearch(query) {
        // 获取当前请求版本号
        const currentVersion = ++requestVersion

        try {
          const response = await mockSearchApi(query)
          //检查是否是最新请求
          if (currentVersion === requestVersion) {
            document.getElementById('results').textContent = response
            console.log(`显示结果: ${response}`)
          } else {
            console.log(`忽略过时结果: ${response} (当前版本: ${requestVersion}, 请求版本: ${currentVersion})`)
          }
        } catch (error) {
          console.error('搜索出错:', error)
        }
      }
      // 监听输入事件
      document.getElementById('searchInput').addEventListener('input', (e) => {
        const query = e.target.value.trim()
        query && raceSafeSearch(query)
      })
    </script>
  </body>
</html>

实现原理

  1. 全局版本号requestVersion 是一个递增的计数器,每次发起新请求时递增

  2. 请求捕获:每次发起请求前,保存当前的 currentVersion

  3. 响应验证:当响应返回时,检查 currentVersion 是否等于最新的 requestVersion

    • 如果相等,说明这是最新的请求结果,可以显示
    • 如果不相等,说明已有更新的请求发出,忽略此结果
  4. 优化:实际使用还可以加上防抖,减少不必要的请求丢弃。

总结

通过这种乐观锁机制,确保了只有最后一次请求的结果会被显示,有效解决了前端搜索中的竞态问题。这种方法简单高效,不需要复杂的状态管理,适用于大多数异步场景下的竞态问题处理。