竞态问题/竞态条件 指的是,当我们在交互过程中,由于各种原因导致同一个接口短时间之内连续发送请求,后发送的请求有可能先得到请求结果,从而导致数据渲染出现预期之外的错误。
因为防止重复执行可以有效的解决竞态问题,因此许多时候面试官也会直接在面试中问我们如何实现防重。常用的方式就是取消上一次请求,或者设置状态让按钮不能连续点击,想必各位大佬对这些方案都已经非常熟悉,我这里就不展开细说。
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 次请求,由于使用串行的策略来解决竞态问题,导致最后一次的请求结果需要等待很长实践才会返回。这无疑极大的降低了开发体验。
和取消上一次的请求相比,无论是从体验上,还是从效率上来说,无疑都是更差的一种方案。
和以往的解决方案,如按钮点击后在请求结果回来之前禁用按钮点击,或取消上一次请求相比,体验要差一点。