React 19 竞态问题解决

竞态问题/竞态条件 指的是,当我们在交互过程中,由于各种原因导致同一个接口短时间之内连续发送请求,后发送的请求有可能先得到请求结果,从而导致数据渲染出现预期之外的错误。

因为防止重复执行可以有效的解决竞态问题,因此许多时候面试官也会直接在面试中问我们如何实现防重。常用的方式就是取消上一次请求,或者设置状态让按钮不能连续点击,想必各位大佬对这些方案都已经非常熟悉,我这里就不展开细说。

React 19 结合 Suspense 也在竞态问题上,提出了一个自己的解决方案。我们结合新的案例来探讨一下这个问题,看完之后大家感受一下这种方式是好是坏。

const getApi = async () => {
  const res = await fetch('https://api.chucknorris.io/jokes/random')
  return res.json()
}

export default function Index() {
  const [api, setApi] = useState(null)
  const [list, setList] = useState([])

  function __clickToGetMessage() {
    setApi(getApi())
  }

  return (
    <div>
      <div id='tips'>点击按钮新增一条数据,该数据从接口中获取</div>
      <button onClick={__clickToGetMessage}>新增数据</button>
      <div className="content">
        <div className="list">
          {list.map((item, index) => {
            return <div className='item' key={item}>{item}</div>
          })}
        </div>
        
        <Suspense fallback={<div>loading...</div>}>
          <Item api={api} setList={setList} />
        </Suspense>
      </div>
    </div>
  )
}

const Item = ({api, setList}) => {
  const [show, setShow] = useState(true)
  const joke = api ? use(api) : {value: 'nothing'}

  useEffect(() => {
    if (!api) return
    setList((list) => {
      if (!list.includes(joke.value)) {
        return list.concat(joke.value)
      }
      return list
    })
    setShow(false)
  }, [])

  const __cls = show ? '_03_a_value show' : '_03_a_value'

  return (
    <div className={__cls}>{joke.value}</div>
  )
}

首先,多次点击会导致多次请求,因此数组中会新增大量的数据。

其次,由于请求太密集,那么点击的先后顺序,与请求成功的先后顺序不一致,因此列表中的顺序也会与点击顺序不同。「竞态问题」

那么我们来试着操作一下,看看该案例会有什么反应。演示结果如下,新增一条数据时,我连续点击了 10 次。

 

结果我们发现,点击期间,并没有新的数据渲染到页面上,一直是 loading 的状态。

再来看一下此时的请求情况。

请求的顺序被严格控制了:上一个请求请求成功之后,下一个请求才开始发生。此时是一个串行的请求过程。

react 19 使用这种思路解决了竞态问题。与此同时,反馈到数据上,虽然前面多次的请求已经成功,但是对于组件状态来说,这个中间过程中一直有请求在发生,此时 React 认为中间的请求产生的数据为无效数据。只会把最后一个请求成功的数据作为最终的返回结果。

 

很显然,仅从 UI 结果上来说,这样的处理方式确实是非常合理的,我们不需要过多的干涉数据的处理,非常的轻松。但问题是,每次请求都成功发生。

当我点击 10 次,就会有 10 次请求,由于使用串行的策略来解决竞态问题,导致最后一次的请求结果需要等待很长实践才会返回。这无疑极大的降低了开发体验。

和取消上一次的请求相比,无论是从体验上,还是从效率上来说,无疑都是更差的一种方案。

和以往的解决方案,如按钮点击后在请求结果回来之前禁用按钮点击,或取消上一次请求相比,体验要差一点。

上一篇:SQLite 嵌入式数据库-一、SQLite 简介


下一篇:网页封装APP:让您的网站变身移动应用