useDeferredValue的功能 & 场景

想象一个非常耗时的组件SlowComponent,当每次重新渲染时,在渲染结束之前,整个JavaScript主线程被阻塞(block),无法处理任何事件,处于假死状态。

所以以下的input在输入新的值时,需要等到所有组件被renderinput框才会更新显示新的值。
SlowComponent耗时大约1s才返回结果,所以从键盘输入 => 到页面上显示结果,需要等待1s
包括input输入框。

function App() {
  const [text, setText] = useState("hello")
  return (
    <div className="App">
      <input value={text} onChange={e => setText(e.target.value)} />
      {/* SlowComponent组件非常耗时 */}
      <SlowComponent text={text} />
    </div>
  )
}
// 通过React.memo优化,text不变时,返回缓存的渲染结果。
const SlowComponent = React.memo(({ text }) => {
  const now = Date.now()
  while (Date.now() - now < 500) {
    // do nothing
  }
  return <div>{text}</div>
})

在线测试,改变1次输入框,输入框需要在500ms后响应。
https://codesandbox.io/s/goofy-khayyam-3q410q?file=/src/App.js

那么可不可以先立刻渲染出input,再去渲染SlowComponent呢?
答案是可以的,将「渲染过程」从上面的1次,分成2次。

怎么分?就是useDeferredValue可以用到的场景:

它可以让一个state在单次渲染中,先返回上一次的值,当渲染任务结束时,再更新最新值,然后再触发一次渲染。

所以得到下面修改后的代码,input框的响应非常及时,不会被SlowComponent阻塞。

import { useDeferredValue } from 'react'

function App() {
  const [text, setText] = useState("hello")
  const deferredText = useDeferredValue(text)
  console.log({text, deferredText})
  return (
    <div className="App">
      <input value={text} onChange={e => setText(e.target.value)} />
      {/* SlowComponent组件非常耗时,当text更改导致re-render时
      deferredText会在第一次渲染时保持上次的值(即不变),
      所以这个组件在第一次是返回缓存的结果,不会非常耗时。
      当第一次渲染结束后,
      再更新最新的deferredText值,从而再触发一次渲染,
      此时才会重新渲染SlowComponent */}
      <SlowComponent text={deferredText} />
    </div>
  )
}

控制台结果

在线测试,改变1次输入框的内容,输入框能过及时得到响应。
https://codesandbox.io/s/awesome-dewdney-rb9epf?file=/src/App.js

手动实现

知道了useDeferredValue的功能,那么这里也能过手动模拟一下它的行为,经测试与原版useDeferredValue效果一致。

function useDeferredValue2(newVal) {
  const [current, setCurrent] = useState(newVal)
  useEffect(() => {
    // 如果值变了,先执行return current(就是上一次的值)
    // 渲染结束后,再更新最新的值,然后再触发一次更新
    setCurrent(newVal)
  }, [newVal])
  return current
}

小结

到这里,就明白了react18的重心是放到了「异步(并发)渲染(concurrent rendering)」上,让更加紧急的渲染先完成,最后再去渲染相对来说不那么紧急的任务。或者是说短任务优先,让耗时小的任务先做,耗时长的任务后做。从而提高页面的响应性(more responsive)。

本文的在线测试链接

未优化,输入框更新响应慢。
https://codesandbox.io/s/goofy-khayyam-3q410q?file=/src/App.js

优化后,输入框更新响应及时。
https://codesandbox.io/s/awesome-dewdney-rb9epf?file=/src/App.js

剩下的问题

优化后,输入框只改变1次时,能够及时响应。但响应完之后渲染SlowComponent的期间,再改变输入框内容,又会被阻塞。 解决办法可以对setText事件做一个防抖,在设定的阈值时间内触发的更改,永远只更新最后一次。