欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

React 數(shù)據(jù)獲取與性能優(yōu)化詳解

 更新時(shí)間:2022年10月17日 16:41:52   作者:KooFE  
這篇文章主要為大家介紹了React 數(shù)據(jù)獲取與性能優(yōu)化方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

引言

如果你嘗試過(guò)對(duì) React 中的數(shù)據(jù)獲取 (data fetching) 進(jìn)行一些思考,就會(huì)發(fā)現(xiàn)它涉及了眾多內(nèi)容:數(shù)據(jù)管理類庫(kù)層出不窮,是否要擁抱 GraphQL;useEffect 是引起瀑布流的禍?zhǔn)?,救世?Suspence 尚處于實(shí)驗(yàn)階段;fetch-on-render、 fetch-then-render render-as-you-fetch 這些模式不由得讓人上頭。那么問(wèn)題來(lái)了,在 React 中獲取數(shù)據(jù)的 “正確方式” 是什么?本文將給予解答。

數(shù)據(jù)獲取的分類

一般來(lái)說(shuō),在現(xiàn)代前端中,我們可以將 “數(shù)據(jù)獲取” 大致分為兩類:初始數(shù)據(jù)獲取 (initial data fetching) 和按需數(shù)據(jù)獲取 (data fetching on demand)。

按需數(shù)據(jù)獲取,是用戶和頁(yè)面發(fā)生交互后再請(qǐng)求數(shù)據(jù),以便提高頁(yè)面的交互體驗(yàn)。所有的自動(dòng)填充、動(dòng)態(tài)表單和內(nèi)容搜索都屬于這一類數(shù)據(jù)。在 React 中,通常是在事件的回調(diào)函數(shù)中請(qǐng)求這些數(shù)據(jù)。

初始數(shù)據(jù)獲取,是在打開頁(yè)面時(shí)我們期望立刻能看到數(shù)據(jù),我們要在組件出現(xiàn)在屏幕之前拿到這些數(shù)據(jù)。這些內(nèi)容對(duì)于用戶體驗(yàn)比較重要,需要盡快展示出來(lái)。在 React 中,通常是在 useEffect (或者 componentDidMount) 中來(lái)發(fā)起這類數(shù)據(jù)請(qǐng)求。

有趣的是,雖然它們?cè)诟拍钌峡雌饋?lái)完全不同,但獲取數(shù)據(jù)的核心原則和基本模式是完全相同的。對(duì)于大部分人來(lái)說(shuō),初始數(shù)據(jù)獲取通常是至關(guān)重要的。在這一階段,你的應(yīng)用程序給用戶留下第一印象,要么是 “慢如黃牛” 要么是 “快如閃電”。正是因?yàn)檫@個(gè)原因,本文將用大量的篇幅來(lái)介紹初始數(shù)據(jù)獲取,以及如何在保證性能的前提下正確的實(shí)現(xiàn)數(shù)據(jù)獲取。

React 獲取數(shù)據(jù)與類庫(kù)支持

首先要回答一個(gè)問(wèn)題,在 React 中要用第三方類庫(kù)來(lái)獲取數(shù)據(jù)嗎?是,也不是。這要取決于我們的具體場(chǎng)景,如果我們只是簡(jiǎn)單的請(qǐng)求一次數(shù)據(jù),那么就不需要第三方庫(kù)支持。在 useEffect 中直接使用 fetch 即可:

const Component = () => {
  const [data, setData] = useState();
  useEffect(() => {
    // fetch data
    const dataFetch = async () => {
      const data = await (
        await fetch(
          "https://run.mocky.io/v3/b3bcb9d2-d8e9-43c5-bfb7-0062c85be6f9"
        )
      ).json();
      // set state when the data received
      setState(data);
    };
    dataFetch();
  }, []);
  return <>...</>
}

但是當(dāng)我們的場(chǎng)景變得復(fù)雜時(shí),就會(huì)面臨一些棘手的問(wèn)題。錯(cuò)誤處理要怎么實(shí)現(xiàn)?如何處理多個(gè)組件從同一個(gè)接口獲取數(shù)據(jù)?這些數(shù)據(jù)是否要緩存?緩存時(shí)間是多久?競(jìng)態(tài)問(wèn)題 (race conditions) 要如何處理?如果要從屏幕上刪除組件,那該怎么辦?應(yīng)該取消這次請(qǐng)求嗎??jī)?nèi)存泄漏又要怎么解決?問(wèn)題諸如此類。

上面提出的問(wèn)題并不只是針對(duì)于 React,這些是網(wǎng)絡(luò)請(qǐng)求中數(shù)據(jù)獲取的常見問(wèn)題。解決這些問(wèn)題(還有更多)只有兩條路:要么重新發(fā)明輪子編寫大量代碼來(lái)解決這些問(wèn)題,要么依靠一些已經(jīng)存在的成熟類庫(kù)。

這些類庫(kù),比如 axios,將對(duì)一些功能進(jìn)行抽象和封裝,如請(qǐng)求取消等,但是并不提供針對(duì) React 的 API。其他類庫(kù),比如 swr,將為我們處理了幾乎所有的事情,包括緩存。但本質(zhì)上,技術(shù)的選擇在這里并不重要。世界尚不存在這樣的類庫(kù),僅通過(guò)自身就能提高應(yīng)用程序的性能。它們只是讓一些事情變得更容易,同時(shí)也讓另外一些事情變得更困難。為了編寫高性能的應(yīng)用程序,我們始終需要了解數(shù)據(jù)獲取的基礎(chǔ)知識(shí)和數(shù)據(jù)編排的模式以及其他相關(guān)的技術(shù)。

React 應(yīng)用的性能

在介紹具體模式和代碼示例之前,讓我們先討論一下應(yīng)用的 “性能” 到底是什么。你如何確定這個(gè)應(yīng)用的 “性能” 是否良好呢?對(duì)于一個(gè)簡(jiǎn)單的組件來(lái)說(shuō),相對(duì)比較直觀的:只需要測(cè)量渲染的耗時(shí)即可。數(shù)字越小,組件 “性能” 越好(速度更快)。數(shù)據(jù)獲取屬于典型的異步操作,在大型應(yīng)用中從用戶體驗(yàn)角度來(lái)看性能,就不是那么直觀了。

假設(shè)我們正在開發(fā)一個(gè)用來(lái)追蹤 issue 的應(yīng)用。頁(yè)面的左側(cè)是一個(gè) Sidebar 側(cè)邊欄,展示了一個(gè)鏈接列表;中間部分是主內(nèi)容區(qū),它的上半部分是用于展示 issue 的詳情區(qū),比如標(biāo)題、描述等;issue 詳情區(qū)的下方是評(píng)論區(qū)。

假如這個(gè)應(yīng)用程序用下面三種不同的方式來(lái)實(shí)現(xiàn):

  • 展示一個(gè) loading 狀態(tài),直至所有數(shù)據(jù)加載完畢,然后一次性渲染出所有數(shù)據(jù)。大約花費(fèi)了 3s。
  • 展示一個(gè) loading 狀態(tài),側(cè)邊欄的數(shù)據(jù)加載完成后,渲染出側(cè)邊欄,然后繼續(xù)保持 loading 狀態(tài),直到中間的內(nèi)容區(qū)域的數(shù)據(jù)加載完成。側(cè)邊欄的需要 1s 的時(shí)間完成渲染,其他部分需要 3s 的時(shí)間。加到一起,大約花費(fèi)了 4s。
  • 展示一個(gè) loading 狀態(tài),加載完主內(nèi)容區(qū)的 issue 并渲染它,保持 loading 狀態(tài)并加載側(cè)邊欄和評(píng)論的數(shù)據(jù)。側(cè)邊欄完成數(shù)據(jù)請(qǐng)求和渲染之后,繼續(xù)為評(píng)論數(shù)據(jù)的加載保持 loading 狀態(tài)。issue 的加載和渲染需要 2s,sidebar 在它之后需要 1s,評(píng)論需要額外的 2s 完成渲染。共計(jì)花費(fèi)了 5s。

那么這個(gè)頁(yè)面用哪種方案來(lái)實(shí)現(xiàn)的性能會(huì)更高呢?你是怎么認(rèn)為的呢?當(dāng)然,這個(gè)答案很棘手,還是要依據(jù)具體情況而定:

第一種實(shí)現(xiàn)方案總共花費(fèi) 3s,是所有實(shí)現(xiàn)方案中最快的。單純從數(shù)字的角度來(lái)看,毫無(wú)疑問(wèn)它是勝出的。但是,在 3s 的時(shí)間里,它沒(méi)有為用戶呈現(xiàn)任何內(nèi)容,這段白屏也是所有實(shí)現(xiàn)方案中最長(zhǎng)的。

第二種實(shí)現(xiàn)方案只用了 1s 就在頁(yè)面上顯示出了一部分內(nèi)容(Sidebar)。從盡可能快地展示內(nèi)容的角度來(lái)看,它無(wú)疑是勝出的。但是,它是所有方案中主內(nèi)容區(qū)耗時(shí)最長(zhǎng)的。

第三種實(shí)現(xiàn)方案中,首先完成了主內(nèi)容區(qū)的 issue 加載。從主內(nèi)容區(qū)的加載速度來(lái)看,它是勝出的。但是,在從左到右的語(yǔ)言中,信息的自然流動(dòng)方向是從左上到右下。這也是我們通常的閱讀方式。這個(gè)頁(yè)面違反了這個(gè)這個(gè)規(guī)則,這帶來(lái)了最糟糕的用戶體驗(yàn)。除此之外,它的加載時(shí)間是最長(zhǎng)的。

對(duì)于方案的選擇,通常取決于我們要向用戶傳遞什么樣的信息。把自己當(dāng)作一個(gè)講故事的人,而頁(yè)面就是我們要講述的故事。這個(gè)故事中最重要的部分是什么呢?次重要的部分又是什么呢?故事的情節(jié)是否連貫?zāi)??你是想拆解成不同的章?jié)講述呢,還是立刻讓用戶看到故事的全貌呢?

只有當(dāng)你對(duì)故事的樣子有所了解,才是將故事整合在一起,并盡可能快地優(yōu)化故事的時(shí)候。同理,應(yīng)用的性能優(yōu)化也是如此。而讓我們解決問(wèn)題的不是各種類庫(kù),Graphql 或 Suspense,而是下面的知識(shí):

  • 開始數(shù)據(jù)獲取的合適的時(shí)機(jī)是什么?
  • 在數(shù)據(jù)獲取正在進(jìn)行時(shí),我們能做些什么?
  • 在數(shù)據(jù)獲取完成之后,我們應(yīng)該做些什么?

以及使用一些技術(shù)手段,讓我們?cè)谶@三個(gè)階段中控制數(shù)據(jù)請(qǐng)求。但在開始介紹這些技術(shù)之前,我們需要了解兩個(gè)更基礎(chǔ)的內(nèi)容:React 生命周期和瀏覽器資源,以及其對(duì)我們目標(biāo)的影響。

React 生命周期與數(shù)據(jù)獲取

在我們?cè)O(shè)計(jì)數(shù)據(jù)請(qǐng)求的方案時(shí),需要特別注意 React 生命周期被觸發(fā)的時(shí)機(jī)。比如下面的代碼:

const Child = () => {
  useEffect(() => {
    // do something here, like fetching data for the Child
  }, []);
  return <div>Some child</div>
};
const Parent = () => {
  // set loading to true initially
  const [isLoading, setIsLoading] = useState(true);
  if (isLoading) return 'loading';
  return <Child />;
}

在 Parent 組件中,Child 組件能否渲染取決于 state 的值。在 Child 組件中,useEffect 里的數(shù)據(jù)請(qǐng)求會(huì)被觸發(fā)嗎?很明顯不會(huì)被觸發(fā)。只有 Parent 組件中的 isLoading 被置為 false 時(shí),Child 組件才會(huì)被渲染并且觸發(fā)數(shù)據(jù)請(qǐng)求。那么再看下面的這段代碼:

const Parent = () => {
  // set loading to true initially
  const [isLoading, setIsLoading] = useState(true);
  // child is now here! before return
  const child = <Child />;
  if (isLoading) return 'loading';
  return child;
}

功能基本完全一致:當(dāng) isLoading 被置為 false 時(shí) 展示 Child,如果為 true 則展示 loading 狀態(tài)。不同的是,把 <Child /> 元素放在了 if 條件的前面。這樣改變之后,Child 組件中的 useEffect 會(huì)被觸發(fā)嗎?答案不是那么明顯,我看到過(guò)很多人在這里糾結(jié)。答案同樣是不能觸發(fā)請(qǐng)求。

盡管我們寫下了 const child = <Child /> 這樣的代碼,但是這句代碼并不會(huì)渲染組件。<Child/> 只不過(guò)是一種語(yǔ)法糖,在函數(shù)中用來(lái)描述將要?jiǎng)?chuàng)建的元素。只有這種描述信息在實(shí)際可見的渲染樹中,它才會(huì)被渲染 -- 比如在組件中作為返回值。在這之前,它什么都不做,安安靜靜地待在那里。

當(dāng)然,還有許多關(guān)于 React 生命周期的事情需要了解:生命周期觸發(fā)的順序是怎樣的,繪制之前和之后會(huì)觸發(fā)哪些內(nèi)容,是什么減慢了什么以及如何觸發(fā)的,LayoutEffect 鉤子怎樣使用等等。但是,當(dāng)你已經(jīng)很好地協(xié)調(diào)了所有事情,在一個(gè)復(fù)雜的應(yīng)用中掙扎了幾秒之后,所有這些就會(huì)變得密切相關(guān)了。在這里就不展開討論這個(gè)問(wèn)題了,否則這篇文章會(huì)變成一本書。

瀏覽器限制和數(shù)據(jù)獲取

也許你會(huì)有這樣的想法:這太復(fù)雜了,難道我們就不能盡快發(fā)出所有請(qǐng)求,并且將數(shù)據(jù)放入某個(gè)全局存儲(chǔ),然后在可用時(shí)使用它?為什么還要為生命周期和請(qǐng)求編排而煩惱呢?

是的,如果這個(gè)應(yīng)用程序很簡(jiǎn)單,并且只需要很少的請(qǐng)求,我們確實(shí)可以這樣做。但在大型應(yīng)用程序中,我們可能有幾十個(gè)數(shù)據(jù)請(qǐng)求,這種實(shí)現(xiàn)方案很可能適得其反。甚至忽視了服務(wù)器的負(fù)載能否可以處理。假設(shè)服務(wù)器可以,問(wèn)題是我們的瀏覽器卻不能!

你知道嗎,瀏覽器對(duì)相同 host 可以處理的并行請(qǐng)求數(shù)是有限制的。假設(shè)服務(wù)器是 HTTP1(仍占互聯(lián)網(wǎng)的70%),那么這個(gè)數(shù)字并沒(méi)有那么大。在 Chrome 中,最多只能有 6 個(gè)并行請(qǐng)求!如果你同時(shí)發(fā)起更多請(qǐng)求,剩下的所有請(qǐng)求都必須排隊(duì),等待可以發(fā)送請(qǐng)求的時(shí)機(jī)。

在一個(gè)大型應(yīng)用程序中,有 6 個(gè)以上的初始數(shù)據(jù)獲取請(qǐng)求并非不合理。在我們上面提到的非常簡(jiǎn)單的 “追蹤 issue 應(yīng)用” 示例中已經(jīng)有 3 個(gè)請(qǐng)求了,我們甚至還沒(méi)有實(shí)現(xiàn)任何有價(jià)值的東西。

想象一下,如果你只是添加了一個(gè)稍微慢一些的用于數(shù)據(jù)分析的請(qǐng)求,在應(yīng)用的最開始它幾乎什么都不做,最終它卻減慢整個(gè)體驗(yàn),你會(huì)不會(huì)整個(gè)人都不好了。

下面是一個(gè)比較簡(jiǎn)單代碼:

const App = () => {
  // I extracted fetching and useEffect into a hook
  const { data } = useData('/fetch-some-data');
  if (!data) return 'loading...';
  return <div>I'm an app</div>
}

假如在 App 中的數(shù)據(jù)請(qǐng)求非???,只用了 50ms。如果在 App 之前再加 6 個(gè)請(qǐng)求,每個(gè)請(qǐng)求耗時(shí)都是 10s,那么整個(gè) App 的加載時(shí)間也要花費(fèi) 10s (當(dāng)然這里是在 Chrome 瀏覽器中運(yùn)行)。

// no waiting, no resolving, just fetch and drop it
fetch('https://some-url.com/url1');
fetch('https://some-url.com/url2');
fetch('https://some-url.com/url3');
fetch('https://some-url.com/url4');
fetch('https://some-url.com/url5');
fetch('https://some-url.com/url6');
const App = () => {
  ... same app code
}

假如我們刪除其中某個(gè)請(qǐng)求,那么請(qǐng)求的時(shí)間就會(huì)降低很多。

出現(xiàn)請(qǐng)求瀑布流的原因

最后,是時(shí)候認(rèn)真編碼了!現(xiàn)在,我們已經(jīng)擁有了所有需要的技術(shù),并知道它們是如何組合在一起的,是時(shí)候開始編寫我們的 Issue 追蹤應(yīng)用了。讓我們來(lái)完成這個(gè)示例,看看如何講述這個(gè)故事。

讓我們首先完成組件布局,然后再進(jìn)行數(shù)據(jù)獲取。我們先實(shí)現(xiàn)應(yīng)用的 App 組件,它將渲染 Sidebar 和 Issue,Issue 中渲染評(píng)論 Comments。

const App = () => {
  return (
    <>
      <Sidebar />
      <Issue />
    </>
  )
}
const Sidebar = () => {
  return // some sidebar links
}
const Issue = () => {
  return <>
    // some issue data
    <Comments />
  </>
}
const Comments = () => {
  return // some issue comments
}

現(xiàn)在來(lái)實(shí)現(xiàn)獲取數(shù)據(jù)功能,首先將 fetch、useEffect 和狀態(tài)管理封裝到一個(gè)自定義 hook 中,代碼如下:

export const useData = (url) => {
  const [state, setState] = useState();
  useEffect(() => {
    const dataFetch = async () => {
      const data = await (await fetch(url)).json();
      setState(data);
    };
    dataFetch();
  }, [url]);
  return { data: state };
};

然后,我們將在更大的組件中請(qǐng)求數(shù)據(jù):在 Issue 組件中請(qǐng)求 issue 數(shù)據(jù),在 Comments 組件中請(qǐng)求評(píng)論列表。當(dāng)然,還要在等待請(qǐng)求結(jié)果的過(guò)程中展示 loading 狀態(tài):

const Comments = () => {
  // fetch is triggered in useEffect there, as normal
  const { data } = useData('/get-comments');
  // show loading state while waiting for the data
  if (!data) return 'loading';
  // rendering comments now that we have access to them!
  return data.map(comment => <div>{comment.title}</div>)
}

在 Issue 中完成同樣的代碼,并且在 loading 結(jié)束之后,渲染 Comments 組件:

const Issue = () => {
  // fetch is triggered in useEffect there, as normal
  const { data } = useData('/get-issue');
  // show loading state while waiting for the data
  if (!data) return 'loading';
  // render actual issue now that the data is here!
  return (
    <div>
      <h3>{data.title}</h3>
      <p>{data.description}</p>
      <Comments />
    </div>
  )
}

App 的代碼如下:

const App = () => {
  // fetch is triggered in useEffect there, as normal
  const { data } = useData('/get-sidebar');
  // show loading state while waiting for the data
  if (!data) return 'loading';
  return (
    <>
      <Sidebar data={data} />
      <Issue />
    </>
  )
}

當(dāng)運(yùn)行這段示例代碼,你會(huì)發(fā)現(xiàn)執(zhí)行起來(lái)很慢。我們這里所實(shí)現(xiàn)的是一個(gè)比較經(jīng)典的請(qǐng)求瀑布流。還記得上面提到的 React 生命周期嗎?組件只有作為返回值被返回時(shí),才會(huì)被掛載和渲染,然后再去執(zhí)行組件內(nèi)部的 useEffect 和 數(shù)據(jù)請(qǐng)求。在這個(gè)實(shí)現(xiàn)方案中,各個(gè)組件在等待請(qǐng)求結(jié)果時(shí),都返回的是 loading 狀態(tài)。只有數(shù)據(jù)加載完成后,子組件才開始在組件樹中渲染。然后子組件開始請(qǐng)求數(shù)據(jù),展示 loading 狀態(tài),重復(fù)著和父組件一樣的過(guò)程。

當(dāng)我們想要盡快的展示應(yīng)用頁(yè)面時(shí),像這樣的瀑布流請(qǐng)求并不是一個(gè)好的解決方案。幸運(yùn)的是,還有其他的方法來(lái)處理這個(gè)問(wèn)題。

解決請(qǐng)求瀑布流的方案

Promise.all 方案

最簡(jiǎn)單的解決方案是,將這些請(qǐng)求盡可能的放在組件樹的最頂層。在我們的示例中,這個(gè)最外層就是根組件 App。值得注意的是,我們并不是簡(jiǎn)簡(jiǎn)單單的將代碼“換”個(gè)位置,還需要做一些額外的處理,像比如下面的代碼是不行的:

useEffect(async () => {
  const sidebar = await fetch('/get-sidebar');
  const issue = await fetch('/get-issue');
  const comments = await fetch('/get-comments');
}, [])

這是另外一種形式的瀑布流,只不過(guò)它位于一個(gè)組件中。當(dāng)我們請(qǐng)求 sidebar 數(shù)據(jù)時(shí)會(huì) await,當(dāng)我們請(qǐng)求 issue 數(shù)據(jù)時(shí)會(huì) await,請(qǐng)求 comment 數(shù)據(jù)時(shí)也會(huì) await。當(dāng)所有數(shù)據(jù)都可用時(shí)才會(huì)開始渲染,這個(gè)過(guò)程要等待的時(shí)間:1s + 2s + 3s = 6s。我們要做的是將所有請(qǐng)求同時(shí)發(fā)出,以并行的方式請(qǐng)求數(shù)據(jù)。這些請(qǐng)求中用時(shí)最長(zhǎng)的時(shí)間,就是我們要等待的時(shí)間:3s,這樣我們的性能就提升了 50%。

可以使用 Promise.all 來(lái)實(shí)現(xiàn):

useEffect(async () => {
  const [sidebar, issue, comments] = await Promise.all([
    fetch('/get-sidebar'),
    fetch('/get-issue'),
    fetch('/get-comments')
  ])
}, [])

然后在父組件中把數(shù)據(jù)提供給 state 并通過(guò) props 傳遞給子組件:

const useAllData = () => {
  const [sidebar, setSidebar] = useState();
  const [comments, setComments] = useState();
  const [issue, setIssue] = useState();
  useEffect(() => {
    const dataFetch = async () => {
      // waiting for allthethings in parallel
      const result = (
        await Promise.all([
          fetch(sidebarUrl),
          fetch(issueUrl),
          fetch(commentsUrl)
        ])
      ).map((r) => r.json());
      // and waiting a bit more - fetch API is cumbersome
      const [sidebarResult, issueResult, commentsResult] = await Promise.all(
        result
      );
      // when the data is ready, save it to state
      setSidebar(sidebarResult);
      setIssue(issueResult);
      setComments(commentsResult);
    };
    dataFetch();
  }, []);
  return { sidebar, comments, issue };
};
const App = () => {
  // all the fetches were triggered in parallel
  const { sidebar, comments, issue } = useAllData()
  // show loading state while waiting for all the data
  if (!sidebar || !comments || !issue) return 'loading';
  // render the actual app here and pass data from state to children
  return (
    <>
      <Sidebar data={state.sidebar} />
      <Issue comments={state.comments} issue={state.issue} />
    </>
  )
}

這也是我們?cè)诒疚拈_始介紹的第一種方案的實(shí)現(xiàn)方式。

并行 Promise 方案

如果我們不想等到所有的數(shù)據(jù)請(qǐng)求都完成,該如何處理呢?comments 的請(qǐng)求比較慢,而且在頁(yè)面中并不重要,因?yàn)樗枞?sidebar 渲染,是極其不明智的。是否能所有請(qǐng)求同時(shí)發(fā)出,各個(gè)組件獨(dú)自等待各自的請(qǐng)求結(jié)果嗎?

當(dāng)然可以,只需將這些 fetch 從 async/await 轉(zhuǎn)移到 Promise.then,在 then 的回調(diào)函數(shù)中處理數(shù)據(jù)。

fetch('/get-sidebar').then(data => data.json()).then(data => setSidebar(data));
fetch('/get-issue').then(data => data.json()).then(data => setIssue(data));
fetch('/get-comments').then(data => data.json()).then(data => setComments(data));

現(xiàn)在每個(gè) fetch 請(qǐng)求都是并行的,在 App 的 render 函數(shù)中我們可用做更多的事情,比如:只要請(qǐng)求的數(shù)據(jù)給到 state,就可以渲染 SiderBar 和 Issue:

const App = () => {
  const { sidebar, issue, comments } = useAllData();
  // show loading state while waiting for sidebar
  if (!sidebar) return 'loading';
  // render sidebar as soon as its data is available
  // but show loading state instead of issue and comments while we're waiting for them
  return (
    <>
      <Sidebar data={sidebar} />
      <!-- render local loading state for issue here if its data not available -->
      <!-- inside Issue component we'd have to render 'loading' for empty comments as well -->
      {issue ? <Issue comments={comments} issue={issue} /> : 'loading''}
    </>
  )
}

在這個(gè)代碼中,只要數(shù)據(jù)準(zhǔn)備好就可以渲染 Sidebar、Issue 和 Comments 組件。這與前面提到的瀑布的行為完全相同。但是由于我們并行的觸發(fā)請(qǐng)求,總耗時(shí)從 6s 降低至 3s。我們大幅的提升了它的性能,同時(shí)保證了功能不受影響。

還有一件事不得不提,在這個(gè)方案中觸發(fā)了三次 state 變化,這會(huì)引起父組件三次重新渲染。考慮到這些重新渲染發(fā)生在頂層組件,像這樣不必要的重新渲染會(huì)引起 App 中較多不必要的重新渲染。始終要牢記,組件的順序和組件的大小都會(huì)對(duì)性能產(chǎn)生影響。想要了解如何解決不必要的重新渲染,可以參考 React 重新渲染指南。

Data providers 抽象封裝數(shù)據(jù)獲取

像上面示例代碼中那樣,將數(shù)據(jù)加載提升到頂層組件,雖然在性能方面有很大的提升,但是對(duì)于應(yīng)用架構(gòu)和代碼可讀性來(lái)說(shuō)簡(jiǎn)直就是噩夢(mèng)。把所有的數(shù)據(jù)請(qǐng)求和大量的 props 放在一起,讓我們得到了一個(gè)巨石組件。

存在一種簡(jiǎn)單的方案:可以為頁(yè)面引入 “data providers” 的概念。在這里,data providers 是對(duì)數(shù)據(jù)請(qǐng)求的一種抽象,能讓我們?cè)?app 中的某個(gè)地方請(qǐng)求數(shù)據(jù),然后在其他地方訪問(wèn)數(shù)據(jù),可以繞過(guò)中間的所有組件。本質(zhì)上就像為每個(gè)請(qǐng)求做了一層迷你的緩存。在原生的 React 中,它其實(shí)就是一個(gè) context:

const Context = React.createContext();
export const CommentsDataProvider = ({ children }) => {
  const [comments, setComments] = useState();
  useEffect(async () => {
    fetch('/get-comments').then(data => data.json()).then(data => setComments(data));
  }, [])
  return (
    <Context.Provider value={comments}>
      {children}
    </Context.Provider>
  )
}
export const useComments = () => useContext(commentsContext);

這三個(gè)請(qǐng)求中的邏輯完全相同,由于篇幅原因其他兩個(gè)的代碼不在這里列出。然后將我們的巨石組件 App 改造得更簡(jiǎn)單:

const App = () => {
  const sidebar = useSidebar();
  const issue = useIssue();
  // show loading state while waiting for sidebar
  if (!sidebar) return 'loading';
  // no more props drilling for any of those
  return (
    <>
      <Sidebar />
      {issue ? <Issue /> : 'loading''}
    </>
  )
}

用這三個(gè)的 provider 來(lái)包裹 App 組件,只要它們被掛載,就會(huì)立即并行請(qǐng)求數(shù)據(jù):

export const VeryRootApp = () => {
  return (
    <SidebarDataProvider>
      <IssueDataProvider>
        <CommentsDataProvider>
          <App />
        </CommentsDataProvider>
      </IssueDataProvider>
    </SidebarDataProvider>
  )
}

在像 Comments 這種組件中(層級(jí)比較深的組件),只需通過(guò) “data provider” 來(lái)訪問(wèn)數(shù)據(jù):

const Comments = () => {
  // Look! No props drilling!
  const comments = useComments();
}

在 React 之前請(qǐng)求數(shù)據(jù)

關(guān)于瀑布流問(wèn)題,最后一條要了解的技巧。把數(shù)據(jù)請(qǐng)求放在 React 前面,會(huì)發(fā)生什么。這是一個(gè)非常危險(xiǎn)的做法,需要謹(jǐn)慎的去使用。

讓我們?cè)倩仡櫼幌?Comments 組件,在我們解決瀑布流問(wèn)題的第一個(gè)方案中,在組件內(nèi)部進(jìn)行數(shù)據(jù)請(qǐng)求和處理:

const Comments = () => {
  const [data, setData] = useState();
  useEffect(() => {
    const dataFetch = async () => {
      const data = await (await fetch('/get-comments')).json();
      setData(data);
    };
    dataFetch();
  }, [url]);
  if (!data) return 'loading';
  return data.map(comment => <div>{comment.title}</div>)
}

注意第 6 行代碼,fetch('/get-comments') 是一個(gè)在 useEffect await 的 promise。在這個(gè)場(chǎng)景中,它并不依賴 React 的任何東西,沒(méi)有 對(duì) props、state 或者其他內(nèi)部變量有依賴。所以,把它移動(dòng)到最頂部,放在聲明 Comments 組件之前,會(huì)發(fā)生什么呢?并且然后在 useEffect 中 await 這個(gè) promise 呢?

const commentsPromise = fetch('/get-comments');
const Comments = () => {
  useEffect(() => {
    const dataFetch = async () => {
      // just await the variable here
      const data = await (await commentsPromise).json();
      setState(data);
    };
    dataFetch();
  }, [url]);
}

發(fā)生了有趣的事情:fetch 基本上 “逃離” 了所有的 React 生命周期,只要頁(yè)面加載完 JavaScript 就會(huì)進(jìn)行數(shù)據(jù)請(qǐng)求,這時(shí)任何的 useEffect 還未被執(zhí)行,甚至根組件 App 中的第一個(gè)請(qǐng)求也還未被發(fā)出。當(dāng)這個(gè)請(qǐng)求發(fā)出去之后,JavaScript 進(jìn)程會(huì)去處理其他事情,而返回的數(shù)據(jù)會(huì)安靜地等待著被 resolve。我們?cè)?Comments 的 useEffect 中做進(jìn)行 resolve。

再看一下第一個(gè)方案的瀑布流的圖:

將 fetch 移到 Comments 的外面:

從技術(shù)角度來(lái)講,可以將所有的 promise 移到組件的外面,這樣就可以解決瀑布流的問(wèn)題,這樣就不需要做請(qǐng)求提升和 data providers。

但是為什么我們不去這樣做呢?為什么它不是一種通用的模式呢?

還記得上面提到的瀏覽器限制嗎,最多只能支持 6 個(gè)請(qǐng)求并行發(fā)出,剩下的請(qǐng)求要放到隊(duì)列中等待。像這種在 React 組件外部發(fā)出的請(qǐng)求,完全是不可控的。在應(yīng)用程序中,一個(gè)請(qǐng)求大量數(shù)據(jù)的組件,基本不可能立即開始渲染,以 “傳統(tǒng)” 瀑布方法實(shí)現(xiàn)該組件,在實(shí)際渲染之前不會(huì)打擾任何人。但如果使用這種 hack 方式,會(huì)存在那些關(guān)鍵的數(shù)據(jù)可能被阻塞的風(fēng)險(xiǎn)。那么就會(huì)帶來(lái)這樣的問(wèn)題:位于某個(gè)角落里毫不起眼、甚至還未被渲染的組件會(huì)拖慢整個(gè)應(yīng)用頁(yè)面。

對(duì)于這種模式,我只能想到兩種 “適用” 場(chǎng)景:在路由層預(yù)加載一些關(guān)鍵資源,以及在 lazy-loaded 組件中預(yù)請(qǐng)求數(shù)據(jù)。

在第一種情況下,您實(shí)際上需要盡快獲取數(shù)據(jù),并且您肯定知道數(shù)據(jù)是比較關(guān)鍵的,并且是立即需要的。而且,lazy-loaded 組件的 JavaScript 只有在它們最終出現(xiàn)在呈現(xiàn)樹中時(shí)才會(huì)被下載和執(zhí)行,所以根據(jù)定義,在獲取和呈現(xiàn)所有關(guān)鍵數(shù)據(jù)之后。所以它是安全的。

使用第三方類庫(kù)做數(shù)據(jù)獲取

直到現(xiàn)在為止,在上面的所有的代碼示例中,我們使用的都是原生 fetch。目的是為了演示 React 中基礎(chǔ)的數(shù)據(jù)請(qǐng)求模式,以及這些模式與類庫(kù)無(wú)關(guān)的。無(wú)論你使用了和將要使用哪種第三方類庫(kù),瀑布流方法的原則、在 React 生命周期內(nèi)外獲取數(shù)據(jù)的方法始終是一致的。

Axios 這樣和 React 無(wú)關(guān)的類庫(kù),是在 fetch 的基礎(chǔ)上做了更復(fù)雜的抽象。我們可以將示例中的 fetch 更換為 axios.get,示例代碼的運(yùn)行結(jié)果不會(huì)有任何變化。

還有一些與 React 集成的類庫(kù),使用了 hooks 和 query-like API,比如 swr 抽象處理了更多的東西: useCallback、state、以及錯(cuò)誤處理和緩存等。而不是像下面這樣一大坨代碼,需要實(shí)現(xiàn)很多東西才能用于生產(chǎn)環(huán)境:

const Comments = () => {
  const [data, setData] = useState();
  useEffect(() => {
    const dataFetch = async () => {
      const data = await (await fetch('/get-comments')).json();
      setState(data);
    };
    dataFetch();
  }, [url]);
  // the rest of comments code
}

使用 swr 則簡(jiǎn)單明了:

const Comments = () => {
  const { data } = useSWR('/get-comments', fetcher);
  // the rest of comments code
}

所有的事情都在底層處理好了:使用 useEffect 請(qǐng)求數(shù)據(jù),將數(shù)據(jù)更新至 state,觸發(fā)組件重新渲染。

關(guān)于 Suspense

在介紹 React 數(shù)據(jù)獲取時(shí),如果不提一下 Suspence,是不完整的。所以 Suspense 解決了哪些數(shù)據(jù)獲取方面的問(wèn)題?并沒(méi)有解決多少問(wèn)題。在寫這篇文章時(shí),Suspense 數(shù)據(jù)獲取還是實(shí)驗(yàn)特性,所以我不建議在任何生產(chǎn)相關(guān)的環(huán)境中使用它。

但讓我們?cè)O(shè)想一下,如果 Suspense 的 API 已經(jīng)固化下來(lái)保持不變,并將在不久后正式發(fā)布。它會(huì)從根本上解決數(shù)據(jù)獲取問(wèn)題嗎?它會(huì)使上述一切問(wèn)題成為歷史嗎?一點(diǎn)也不會(huì)。因?yàn)?,我們?nèi)匀粫?huì)受到瀏覽器資源限制、受到 React 生命周期以及請(qǐng)求瀑布流的限制。

Suspense 只是一種非常復(fù)雜和聰明的方式,用來(lái)取代對(duì) loading 狀態(tài)的處理。對(duì)于下面的代碼:

const Comments = ({ commments }) => {
  if (!comments) return 'loading';
  // render comments
}

只需提升 loading 狀態(tài)并執(zhí)行以下操作:

const Issue = () => {
  return <>
    // issue data
    <Suspence fallback="loading">
      <Comments />
    </Suspence>
  </>
}

在使用 Suspence 時(shí),也不要忘記上面的所有核心原則和限制。

總結(jié)

接下來(lái)對(duì)本文的重點(diǎn)做一個(gè)總結(jié):

  • 在 React 中獲取數(shù)據(jù)不一定要用第三方類庫(kù),但它們也很有用
  • 性能是主觀的,始終取決于用戶體驗(yàn)
  • 可用的瀏覽器資源有限,只有 6 個(gè)并行請(qǐng)求;不要濫用 pre-fetching
  • useEffect 本身不會(huì)導(dǎo)致瀑布,它們只是組件組合和 loading 狀態(tài)帶來(lái)的自然結(jié)果

以上就是React 數(shù)據(jù)獲取與性能優(yōu)化詳解的詳細(xì)內(nèi)容,更多關(guān)于React 數(shù)據(jù)獲取性能優(yōu)化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • React Draggable插件如何實(shí)現(xiàn)拖拽功能

    React Draggable插件如何實(shí)現(xiàn)拖拽功能

    這篇文章主要介紹了React Draggable插件如何實(shí)現(xiàn)拖拽功能問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-07-07
  • React immer與Redux Toolkit使用教程詳解

    React immer與Redux Toolkit使用教程詳解

    這篇文章主要介紹了React中immer與Redux Toolkit的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧
    2022-10-10
  • React結(jié)合Drag?API實(shí)現(xiàn)拖拽示例詳解

    React結(jié)合Drag?API實(shí)現(xiàn)拖拽示例詳解

    這篇文章主要為大家介紹了React結(jié)合Drag?API實(shí)現(xiàn)拖拽示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-03-03
  • React 高階組件HOC用法歸納

    React 高階組件HOC用法歸納

    高階組件就是接受一個(gè)組件作為參數(shù)并返回一個(gè)新組件(功能增強(qiáng)的組件)的函數(shù)。這里需要注意高階組件是一個(gè)函數(shù),并不是組件,這一點(diǎn)一定要注意,本文給大家分享React 高階組件HOC使用小結(jié),一起看看吧
    2021-06-06
  • React Render Props共享代碼技術(shù)

    React Render Props共享代碼技術(shù)

    render props是指一種在 React 組件之間使用一個(gè)值為函數(shù)的 prop 共享代碼的技術(shù)。簡(jiǎn)單來(lái)說(shuō),給一個(gè)組件傳入一個(gè)prop,這個(gè)props是一個(gè)函數(shù),函數(shù)的作用是用來(lái)告訴這個(gè)組件需要渲染什么內(nèi)容,那么這個(gè)prop就成為render prop
    2023-01-01
  • React利用路由實(shí)現(xiàn)登錄界面的跳轉(zhuǎn)

    React利用路由實(shí)現(xiàn)登錄界面的跳轉(zhuǎn)

    這篇文章主要介紹了React利用路由實(shí)現(xiàn)登錄界面的跳轉(zhuǎn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2021-04-04
  • react?native?reanimated實(shí)現(xiàn)動(dòng)畫示例詳解

    react?native?reanimated實(shí)現(xiàn)動(dòng)畫示例詳解

    這篇文章主要為大家介紹了react?native?reanimated實(shí)現(xiàn)動(dòng)畫示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-03-03
  • 在Create React App中使用CSS Modules的方法示例

    在Create React App中使用CSS Modules的方法示例

    本文介紹了如何在 Create React App 腳手架中使用 CSS Modules 的兩種方式。有一定的參考價(jià)值,有需要的朋友可以參考一下,希望對(duì)你有所幫助。
    2019-01-01
  • react中的雙向綁定你真的了解嗎

    react中的雙向綁定你真的了解嗎

    這篇文章主要為大家詳細(xì)介紹了react中的雙向綁定,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助
    2022-03-03
  • 瀏覽器中視頻播放器實(shí)現(xiàn)的基本思路與代碼

    瀏覽器中視頻播放器實(shí)現(xiàn)的基本思路與代碼

    這篇文章主要給大家介紹了關(guān)于瀏覽器中視頻播放器實(shí)現(xiàn)的基本思路與代碼,并且詳細(xì)總結(jié)了瀏覽器中的音視頻知識(shí),對(duì)大家的理解和學(xué)習(xí)非常有幫助,需要的朋友可以參考下
    2021-08-08

最新評(píng)論