Fwio

Vue 与 React 与竞态问题

Reference

竞态问题(Race Condition)

竞态问题是异步编程的典型问题,两个请求相互”竞争”,最后以无法预料的顺序返回或解决。

在处理竞态问题上,Vue 和 React 都用到了”过期标志”的方法。

React 处理竞态问题

You Might Not Need An Effect 中, React 为竞态问题举了一个”搜索框”的例子。

假设用户对单词"hello"以一种完美的、绕过你自信的防抖或者节流策略的节奏敲下键盘, 那么"h""he""hel""hell""hello"都将触发不同的 data fetching。

显然它们返回响应的时间是无法预测的,不能简单地以最后一个响应(它可能是过期的)为准。

function SearchResults({ query }) {
  const [results, setResults] = useState([])
  const [page, setPage] = useState(1)

  useEffect(() => {
    // Look
    // A race condition
    // Right here
    fetchResults(query, page).then((json) => {
      setResults(json)
    })
  }, [query, page])
}

对此,React 要求开发者自己为 Effects 手动设置正确的 cleanup 函数。

function SearchResults({ query }) {
  const [results, setResults] = useState([])
  const [page, setPage] = useState(1)

  useEffect(() => {
    // a stale flag
    let ignore = false

    fetchResults(query, page).then((json) => {
      // Apply the results only when it's not expired
      if (!ignore) {
        setResults(json)
      }
    })

    // cleanup function
    return () => {
      // re-rendering means current fetch is expired
      ignore = true
    }
  }, [query, page])
}

在这个模式(pattern)中,当query改变导致<SearchResults />被重新渲染时,Effect 的 cleanup 函数标记其请求已经过期ignore = true), 这样,当客户端收到响应时,便不会采用该过期的响应。

这种模式运用闭包let ignore)对异步代码(then())进行了分支。当然,它只是丢弃了过期的数据,并不能阻止发送请求

Vue 处理竞态问题

在《Vue.js 设计与实现》的 4.11 过期的副作用 中,以watch API 为例讲述了 Vue 内部对竞态问题的处理。

在下面的片段中,短时间内多次操作obj也会发起多次 data fetching,产生竞态问题。

let finalData

watch(obj, async () => {
  // 发送并等待网络请求
  const res = await fetch('/path/to/request')
  // 将请求结果赋值给 data
  finalData = res
})

Vue 3 为这种情况专门提供了一个类似于 cleanup 函数的onInvalidate,它是watch回调的第三个可选参数,是一个用来注册回调函数的函数。

onInvalidate的原理是:在watch内部每次检测到变更后,在副作用函数执行之前,会先调用通过onInvalidate注册的回调函数。

很容易想到,这和上面 React Effects 的 cleanup 函数原理几乎一模一样。

watch(obj, async (newValue, oldValue, onInvalidate) => {
  let expired = false

  // 副作用重新执行之前,执行该回调
  onInvalidate(() => {
    // 闭包
    expired = true
  })

  const res = await fetch('/path/to/request')
  //
  if (!expired) {
    finalData = res
  }
})

两者的解决方法几乎一模一样,区别只在于 Vue 注册 cleanup 函数的 API 形式较为特殊,是传递给watch的回调函数的一个类似于 hook 的可选参数, 比起 React 更加的集成,但都应用了闭包的思维。