自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

有點(diǎn)東西??!一個(gè)被小瞧的冷門(mén)Hook 補(bǔ)全了 React 19 異步優(yōu)秀實(shí)踐的最后一環(huán)

開(kāi)發(fā) 前端
useTransition 能夠阻止 Suspense 在請(qǐng)求發(fā)生時(shí),渲染 fallback 中的 Loading 組件,并且,isPending 也能表示請(qǐng)求正在發(fā)生,因此,我把 isPending 傳入到子組件中,那么我們就可以在子組件中自定義請(qǐng)求狀態(tài)。

先預(yù)警一下,完全消化本文內(nèi)容有點(diǎn)難。

  • useDeferredValue 解決真實(shí)場(chǎng)景問(wèn)題的案例。
  • useDeferredValue 基礎(chǔ)知識(shí)。
  • 復(fù)雜案例渲染過(guò)程分析。
  • useDeferredValue 底層執(zhí)行原理分析。
  • 重新分析取消請(qǐng)求案例。

全文共 5104 字,閱讀需要花費(fèi) 10 分鐘。

useDeferredValue,一個(gè)出了很久,但是我?guī)缀鯖](méi)咋在實(shí)踐中用到過(guò)的超冷門(mén) hook。它有多冷門(mén)呢,我之前甚至都覺(jué)得沒(méi)必要介紹它。

直到前幾天,一個(gè)粉絲給了我重要的思路,我才認(rèn)識(shí)到它的威力,逐漸深入了解之后發(fā)現(xiàn)它簡(jiǎn)直就是一個(gè)寶藏 hook,說(shuō)它是為了 Suspense 量身訂做的都不為過(guò)。

原來(lái)我一直都小瞧了它....

它的存在,直接補(bǔ)齊了 React 19 新架構(gòu)思維最佳實(shí)踐的最后一塊短板。

正因?yàn)檎J(rèn)識(shí)到了它的重要性,所以我迫不及待的想把它分享給大家。

一、遇到了一個(gè)問(wèn)題

如圖所示,在之前的案例中,我想要實(shí)現(xiàn)這樣一個(gè)功能:當(dāng)我快速在輸入框中輸入內(nèi)容時(shí),我希望請(qǐng)求能自動(dòng)發(fā)生,并且請(qǐng)求發(fā)生時(shí),之前存在的列表不能被替換為 Loading 組件。

此時(shí),我使用 useTransition 勉強(qiáng)實(shí)現(xiàn)了該功能。主要代碼如下:

export default function Index() {
  const [api, setApi] = useState(postApi)
  const [isPending, startTransition] = useTransition()

  function __inputChange() {
    startTransition(() => {
      api.cancel()
      setApi(postApi())
    })
  }
  ....
<Suspense fallback={<div>loading...</div>}>
  <List api={api} isPending={isPending} />
</Suspense>
const List = ({api, isPending}) => {
  const posts = use(api)
  
  return (
    <ul className='_04_list' style={{opacity: isPending ? 0.5 : 1}}>
      {posts.map((post) => (
        <div key={post.id} className='_04_item'>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
    </ul>
  )
}

useTransition 能夠阻止 Suspense 在請(qǐng)求發(fā)生時(shí),渲染 fallback 中的 Loading 組件,并且,isPending 也能表示請(qǐng)求正在發(fā)生,因此,我把 isPending 傳入到子組件中,那么我們就可以在子組件中自定義請(qǐng)求狀態(tài)。

這基本達(dá)到了我想要的交互效果。

但是一個(gè)嚴(yán)重的問(wèn)題是,我每次輸入,都會(huì)發(fā)送一個(gè)請(qǐng)求,當(dāng)我快速輸入時(shí),我希望通過(guò)取消上一次還沒(méi)完成的請(qǐng)求的方式來(lái)優(yōu)化交互效果。useTransition 并不支持我這樣做。

核心原因是因?yàn)?useTransition 的任務(wù)會(huì)排隊(duì)依次執(zhí)行,當(dāng)我想要在下一個(gè)任務(wù)開(kāi)始時(shí),取消上一個(gè)請(qǐng)求時(shí),上一個(gè)任務(wù)已經(jīng)執(zhí)行完了。因此 api.cancel() 雖然成功執(zhí)行了,但是并起不到取消請(qǐng)求的效果,它執(zhí)行時(shí),已經(jīng)沒(méi)有未完成的請(qǐng)求了。

useTransition 無(wú)法取消請(qǐng)求。我思考了很久,也沒(méi)摸索出來(lái)一個(gè)合適的方案。因此之前我只能使用防抖來(lái)做這個(gè)優(yōu)化。

const [api, setApi] = useState(postApi)
const [isPending, startTransition] = useTransition()
const timer = useRef(null)

function __inputChange() {
  clearTimeout(timer.current)
  timer.current = setTimeout(() => {
    startTransition(() => {
      api.cancel()
      setApi(postApi())
    })
  }, 300)  
}
...

但是很顯然,這不是很優(yōu)雅,因?yàn)榉蓝秾?shí)際上和 useTransition 有類(lèi)似的作用,用了防抖之后,useTransition 在這里的存在就變得有點(diǎn)尷尬了。

意外之喜的是,有大佬級(jí)別的粉絲在評(píng)論區(qū)給我提供了一個(gè)非常優(yōu)雅的解決思路。那就是利用 useDeferredValue。

按照粉絲的思路,我把代碼改造如下:

export default function Index() {
  const [api, setApi] = useState(postApi)
  const deferred = useDeferredValue(api)

  function __inputChange(e) {
    api.cancel()
    setApi(postApi())
  }
  ...
<Suspense fallback={<div>loading...</div>}>
  <List api={deferred} isPending={api !== deferred} />
</Suspense>

驗(yàn)證之后發(fā)現(xiàn),我靠,成了!

肅然起敬?。。?!

在保證了代碼優(yōu)雅的情況之下,輕松實(shí)現(xiàn)了我理想中的效果。useDeferredValue 直接補(bǔ)齊了 React 19 異步開(kāi)發(fā)中,最佳實(shí)踐的最后一塊短板!

代碼就這么幾行,但是要理解 useDeferredValue,可能就要花點(diǎn)時(shí)間了。我們一起來(lái)學(xué)習(xí)一下。

二、useDeferredValue 基礎(chǔ)

useDeferredValue 是一個(gè)可以推遲 UI 更新的 hook。這句話理解起來(lái)有點(diǎn)困難。需要我稍微給各位道友解讀一下。

在正常情況下,一個(gè) state 的變化,會(huì)導(dǎo)致 UI 發(fā)生變化。例如下面這個(gè)案例。

function Index() {
  const [counter, setCounter] = useState(0)

  function __clickHanler() {
    setCounter(counter + 1)
  }

  return (
    <div>
      <div id='tips'>基礎(chǔ)案例,state 遞增</div>
      <button onClick={__clickHanler}>counter++</button>
      <div className="counter">counter: {counter}</div>
      <div className="counter">counter: {counter}</div>
    </div>
  )
}

這里需要注意的是,狀態(tài) counter 被兩個(gè)元素使用,因此,這兩個(gè)元素的更改,實(shí)際上是一個(gè)任務(wù)。他們必定會(huì)同時(shí)響應(yīng) counter 的變化。

但是這個(gè)時(shí)候,我們可以利用 useDeferredValue,把他們拆分成兩個(gè)任務(wù)。

function Index() {
  const [counter, setCounter] = useState(0)
  const deferred = useDeferredValue(counter)

  function __clickHanler() {
    setCounter(counter + 1)
  }

  return (
    <div>
      <div id='tips'>基礎(chǔ)案例,state 遞增</div>
      <button onClick={__clickHanler}>counter++</button>
      <div className="counter">
        counter: {counter}
      </div>
      <div className="counter">
        counter: {deferred}
      </div>
    </div>
  )
}

注意看,我們使用 counter 作為 useDeferredValue 的初始值,并將其返回值替換第二個(gè)元素。

const deferred = useDeferredValue(counter)
<div className="counter">
  counter: {deferred}
</div>

此時(shí),第二個(gè)元素的更新,就不再與第一個(gè)元素同步。它更新的優(yōu)先級(jí)被降低。這個(gè)時(shí)候它的執(zhí)行在理論上是可以被更高的優(yōu)先級(jí)插隊(duì)和中斷的。

但是由于渲染都太短了,我們?nèi)庋蹮o(wú)法區(qū)分出來(lái)兩個(gè)任務(wù)已經(jīng)被分開(kāi)了,因此我們把第二個(gè)元素重構(gòu)成一個(gè)子組件,并模擬成一個(gè)耗時(shí)組件。此時(shí)我們就能明顯看出區(qū)別來(lái)。

<Expensive counter={deferred} />
const Expensive = ({counter}) => {
  const start = performance.now()
  while (performance.now() - start < 200) {}
  return (
    <div className="counter">Deferred: {counter}</div>
  )
}

演示效果如下。

因此,我們可以利用 useDeferredValue 推遲 UI 的更新。將對(duì)應(yīng)任務(wù)的優(yōu)先級(jí)降低,使其可以被插隊(duì)與中斷。

三、復(fù)雜案例分析

在這里,我們要更加清楚的理解任務(wù)和渲染任務(wù),才能對(duì)案例的分析更加的精準(zhǔn)。以上一個(gè)例子的 Expensive 組件為例。

狀態(tài)變化時(shí),diff 會(huì)發(fā)生,Expensive 函數(shù)本身作為 diff 過(guò)程的一部分,它必定也會(huì)執(zhí)行,但是這里我們注意,它對(duì)應(yīng)的渲染任務(wù),卻是可以被阻止執(zhí)行的。

例如在上面的例子中,當(dāng)我快速點(diǎn)擊按鈕遞增時(shí),Expensive 組件不會(huì)依次遞增。效果如下:

我們發(fā)現(xiàn),Expensive 組件的渲染直接從 0 變成了 7。

這是因?yàn)樽鳛橐粋€(gè)耗時(shí)任務(wù),又被標(biāo)記了低優(yōu)先級(jí),因此它的渲染任務(wù)不停的被優(yōu)先級(jí)更高的 counter 中斷并放棄。因此直接從 0 變成了 7。

但是此時(shí)我們也發(fā)現(xiàn)另外一個(gè)情況,那就是 counter 直接對(duì)應(yīng)的高優(yōu)先級(jí)執(zhí)行也沒(méi)有那么流暢,這是為什么呢?其實(shí)很簡(jiǎn)單,因?yàn)樵谖覀兊哪M案例中,并沒(méi)有把耗時(shí)定位在渲染上。這可能和實(shí)踐情況會(huì)不太一樣。我們把耗時(shí)寫(xiě)在了 Expensive 函數(shù)里,而這個(gè)函數(shù)每次都會(huì)執(zhí)行,它的執(zhí)行阻塞了渲染。

const Expensive = ({counter}) => {
  const start = performance.now()
  while (performance.now() - start < 200) {}
  return (
    <div className="counter">Deferred: {counter}</div>
  )
}

?

所以這里我們一定要區(qū)分開(kāi)渲染任務(wù)和 Expensive 函數(shù),他們是不同的,UI 渲染是一個(gè)異步任務(wù),而 Expensive 函數(shù)是同步執(zhí)行的。useDeferredValue 推遲的是 UI 渲染任務(wù)。因此,我們需要特別注意的是,不要在同步邏輯上執(zhí)行過(guò)多的耗時(shí)任務(wù)。

但是我們可以通過(guò)任務(wù)拆分的方式,把執(zhí)行耗時(shí)時(shí)間分散到更多的子組件中去,這樣 React 就可以利用任務(wù)中斷的機(jī)制,在不阻塞渲染的情況下,中斷低優(yōu)先級(jí)的任務(wù)。

借用官網(wǎng)的一個(gè)復(fù)雜案例來(lái)跟大家演示。

function SlowList({ text }) {
  // Log once. The actual slowdown is inside SlowItem.
  console.log('[ARTIFICIALLY SLOW] Rendering 250 <SlowItem />');

  let items = [];
  for (let i = 0; i < 250; i++) {
    items.push(<SlowItem key={i} text={text} />);
  }
  return (
    <ul className="items">
      {items}
    </ul>
  );
}

function SlowItem({ text }) {
  let startTime = performance.now();
  while (performance.now() - startTime < 1) {
    // Do nothing for 1 ms per item to emulate extremely slow code
  }

  return (
    <li className="item">
      Text: {text}
    </li>
  )
}

此時(shí)我們注意觀察,不要錯(cuò)漏這個(gè)細(xì)節(jié)。slowList 中包含了 250 個(gè)子組件。每個(gè)子組件都渲染 1ms,那么整個(gè)組件渲染就需要耗時(shí)至少 250ms。

在父組件中,我們把 deferred 傳遞給 SlowList。

<SlowList text={deferred} />

那么此時(shí)表示,slowList 的任務(wù)是低優(yōu)先級(jí)。counter 對(duì)應(yīng)的任務(wù)可以中斷它的執(zhí)行。當(dāng)我快速點(diǎn)擊時(shí),執(zhí)行效果如下。

此時(shí)一個(gè)很明顯的區(qū)別就是,counter 的 UI 變化變得更加流暢了。這是因?yàn)楹臅r(shí)被拆分到了多個(gè)子組件中,React 就有機(jī)會(huì)中斷這些函數(shù)的執(zhí)行,并執(zhí)行優(yōu)先級(jí)更高的任務(wù),以確保高優(yōu)先級(jí)任務(wù)的流暢。

如果你沒(méi)有使用 React Compiler,你需要使用 memo 手動(dòng)緩存 SlowList。

const SlowList = memo(function SlowList({ text }) {
  // ...
});

useDefferdValue 會(huì)首先使用舊值傳遞給組件。

<SlowList text={deferred} />

因此,當(dāng) counter 發(fā)生變化時(shí),deferred 依然是舊值,那么此時(shí),如果我們使用 memo 包裹,SlowList 的 props 就沒(méi)有發(fā)生變化,我們可以跳過(guò)此次針對(duì) SlowList 的更新。

這跟 React 的性能優(yōu)化策略有關(guān)。

四、運(yùn)行原理

看了上面兩個(gè)例子,肯定還是有一部分人會(huì)覺(jué)得很懵,不要急,接下來(lái)我們把運(yùn)行原理分析一下,整個(gè)情況就清晰了。

useDeferredValue 會(huì)嘗試將 UI 任務(wù)更新兩次。

第一次,會(huì)給子組件傳遞舊值。此時(shí) SlowList 接收到的 props 會(huì)與上一次完全相同。如果結(jié)合了 React.memo,那么該組件就不會(huì)重新渲染。該組件可以重復(fù)使用之前的渲染結(jié)果。

?

Compiler 編譯之后不需要 memo。

此時(shí),高優(yōu)先級(jí)的任務(wù)渲染會(huì)發(fā)生,渲染完成之后,將會(huì)開(kāi)始第二次渲染。此時(shí),將會(huì)傳入剛才更新之后的新值。對(duì)于 SlowList 而言,props 發(fā)生了變化,整個(gè)組件會(huì)重新渲染。

我們通常會(huì)將已經(jīng)非常明確的耗時(shí)任務(wù)標(biāo)記為 deferred,因此,這些任務(wù)都被視為低優(yōu)先級(jí)。當(dāng)重要的高優(yōu)先級(jí)更新已經(jīng)完成,低優(yōu)先級(jí)任務(wù)在第二次渲染時(shí)嘗試更新...

在它第二次更新的過(guò)程中,如果又有新的高優(yōu)先級(jí)任務(wù)進(jìn)來(lái),那么 React 就會(huì)中斷并放棄第二次更新,去執(zhí)行高優(yōu)先級(jí)的任務(wù)。

i

注意:是中斷,并放棄這次更新,所以表現(xiàn)出來(lái)的結(jié)果就是,中間會(huì)漏掉許多任務(wù)的執(zhí)行。

這樣的運(yùn)行機(jī)制有一個(gè)非常重要的好處。

那就是,如果你的電腦性能足夠強(qiáng)悍,那么第二次的更新可能會(huì)快速完成,高優(yōu)先級(jí)的任務(wù)來(lái)不及中斷,那么我們的頁(yè)面響應(yīng)就是非常理想的。

但是如果我們的電腦性能比較差,第二次更新還沒(méi)完成,新的高優(yōu)先級(jí)任務(wù)又來(lái)了,那么就可以通過(guò)中斷的方式,降級(jí)處理,保證重要 UI 的流暢,放棄低優(yōu)先級(jí)任務(wù)。

?

在不同性能的設(shè)備上,有不同的反應(yīng),這個(gè)是跟防抖、節(jié)流的最重要的區(qū)別。

五、重新分析取消請(qǐng)求案例

那我們回過(guò)頭來(lái),分析一下最開(kāi)始的那個(gè)案例,重新看一眼代碼

export default function Index() {
  const [api, setApi] = useState(postApi)
  const deferred = useDeferredValue(api)

  function __inputChange(e) {
    api.cancel()
    setApi(postApi())
  }
  ...
<Suspense fallback={<div>loading...</div>}>
  <List api={deferred} isPending={api !== deferred} />
</Suspense>

這里我們將 api 做為 state,當(dāng) api 被重新賦值時(shí),List 會(huì)經(jīng)歷兩次更新。

首先點(diǎn)擊事件觸發(fā),請(qǐng)求立即發(fā)生。api 被改變。觸發(fā)組件更新。

第一次更新時(shí),deferred 使用舊值傳參,此時(shí)對(duì)于 List 而言,api 沒(méi)有發(fā)生變化。因此,利用這個(gè)機(jī)制,我們可以阻止 Suspense 直接渲染成 fallback。

在 Suspense 包裹之下,只有當(dāng)接口請(qǐng)求成功之后,deferred 的第二次更新才會(huì)發(fā)生,因此,在這個(gè)過(guò)程中,如果我們快速進(jìn)行第二次點(diǎn)擊,可以直接取消上一次請(qǐng)求,讓第二次更新來(lái)不及執(zhí)行。此時(shí)新的請(qǐng)求發(fā)生。

?

這里要結(jié)合 Suspense 的執(zhí)行機(jī)制來(lái)理解。

六、總結(jié)

這種場(chǎng)景的最佳實(shí)踐代碼非常的簡(jiǎn)潔和優(yōu)雅。寫(xiě)起來(lái)也很舒服,性能也非常強(qiáng)悍。但是理解起來(lái)會(huì)比較困難。因此想要做到靈活運(yùn)用,還需要多多消化。

但是,等你徹底掌握它之后,你就會(huì)發(fā)現(xiàn) React 19 在異步交互上真的太優(yōu)雅了。這樣的開(kāi)發(fā)體驗(yàn),是依賴(lài) useEffect 完全比不了的。

后續(xù)的分享中,我將會(huì)繼續(xù)為大家分享 React Action 的設(shè)計(jì)核心思維與具體使用。

責(zé)任編輯:姜華 來(lái)源: 這波能反殺
相關(guān)推薦

2024-04-15 12:54:00

ReactVue列表邏輯

2024-03-19 00:00:00

ReactJavaScript開(kāi)發(fā)

2021-08-30 13:00:40

JS代碼前端

2024-06-12 07:44:28

2021-03-31 08:42:44

IT安全網(wǎng)絡(luò)安全網(wǎng)絡(luò)攻擊

2022-08-02 09:55:04

React前端

2022-06-06 09:28:36

ReactHook

2022-03-16 17:01:35

React18并發(fā)的React組件render

2021-03-02 08:39:42

通信監(jiān)控網(wǎng)絡(luò)

2011-05-27 13:09:14

Google Wall移動(dòng)支付谷歌

2023-12-21 11:44:16

緩存系統(tǒng)設(shè)計(jì)系統(tǒng)

2024-07-22 07:05:00

nodejs爬蟲(chóng)管理平臺(tái)爬蟲(chóng)框架

2024-10-11 14:33:15

ReactRemix用戶(hù)

2024-11-28 10:26:32

2013-07-01 11:01:22

API設(shè)計(jì)API

2020-09-08 13:13:29

監(jiān)控網(wǎng)絡(luò)數(shù)據(jù)

2014-12-17 10:35:17

大數(shù)據(jù)分析 HadooApacheSqoop

2011-10-18 10:19:37

2023-03-30 08:00:00

ReactJavaScript前端

2012-11-09 09:23:22

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)