<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          React18正式版源碼級剖析

          共 9341字,需瀏覽 19分鐘

           ·

          2022-04-04 06:28

          本文適合對React18.0.0源碼感興趣的小伙伴閱讀。

          歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進(jìn)階~

          一、前言

          本文是廣東靚仔的好友bubucuo-高老師寫的,高老師最近在準(zhǔn)備React18的視頻,有興趣的小伙伴可以去學(xué)習(xí)學(xué)習(xí)。


          React18最重要的改變必須是Concurrent,就像哪吒降生一樣,打磨了很長時間了,終于正式見人了。


          a63be0e85f00f2cfc2dc54f51910b4b5.webp


          Concurrent Or Concurrency,中文我們通常翻譯為并發(fā),也有少部分翻譯成并行。React已經(jīng)著手開發(fā)Concurrent幾年了,但是一直只存在于實驗版本。到了React18,Concurrent終于正式投入使用了。


          Concurrent并不是API之類的特性,而是一種能讓你的React項目同時具有多個版本UI的幕后機制,相當(dāng)愛迪生背后的特斯拉。


          Concurrent很重要,雖然它不是API之類的新特性,但是如果你想解鎖React18的大部分新特性,諸如transition、Suspense等,背后就要依賴Concurrent這位大佬。


          React雖然一直在強調(diào)開發(fā)者并不真的需要了解Concurrent是什么,但是忽然來了一句:

          49ab86599eaa739ae8b4584e9fbc29e6.webp


          是的,如果你不想追求high level,就別學(xué)了。

          二、Concurrent

          什么是Concurrent

          Concurrent最主要的特點就渲染是可中斷的。沒錯,以前是不可中斷的,也就是說,以前React中的update是同步渲染,在這種情況下,一旦update開啟,在任務(wù)完成前,都不可中斷。

          注意:這里說的同步,和setState所謂的同步異步不是一碼事,而且setState所謂的異步本質(zhì)上是個批量處理。

          Concurrent模式特點

          在Concurrent模式下,update開始了也可以中斷,晚點再繼續(xù)嘛,當(dāng)然中間也可能被遺棄掉。


          關(guān)于可中斷

          先說可中斷這件事情的重要性。對于React來說,任務(wù)可能很多,如果不區(qū)分優(yōu)先級,那就是先來后到的順序。雖然聽起來很合理,但是現(xiàn)實是普通車輛就應(yīng)該給救護(hù)車讓路,因為事有輕重緩急嘛。那么在React中呢,如果高優(yōu)先級任務(wù)來了,但是低優(yōu)先級任務(wù)還沒有處理完畢,就會造成高優(yōu)先級任務(wù)等待的局面。比如說,某個低優(yōu)先級任務(wù)還在緩慢中,input框忽然被用戶觸發(fā),但是由于主線程被占著,沒有人搭理用戶,結(jié)果是用戶哐哐輸入,但是input沒有任何反應(yīng)。用戶一怒之下就走了,那你那個低優(yōu)先級的任務(wù)還更新個什么呢,用戶都沒了。


          由此可見,對于復(fù)雜項目來說,任務(wù)可中斷這件事情很重要。那么問題來了,React是如何做到的呢,其實基礎(chǔ)還是fiber,fiber本身鏈表結(jié)構(gòu),就是指針嘛,想指向別的地方加個屬性值就行了。


          關(guān)于被遺棄

          在Concurrent模式下,有些update可能會被遺棄掉。先舉個??:
          比如說,我看電視的時候,切換遙控器,從1頻道切換到2頻道,再切換到3頻道,最后在4頻道停下來。假如這些頻道都是UI,那么2、3頻道的渲染其實我并不關(guān)心,我只關(guān)心4頻道的結(jié)果,如果你非要花時間把2和3頻道的UI也渲染出來,最終導(dǎo)致4頻道很久之后才渲染出來,那我肯定不開心。正確的做法應(yīng)該是盡快渲染4頻道就行了,至于2和3頻道,不管渲染了多少了,遺棄了就行了,反正也不需要了。
          最后回到項目的實際場景,比如我想在淘寶搜索“老人與海”,那么我在輸入框輸入“老人與海”的過程中,“老人”會有對應(yīng)的模糊查詢結(jié)果,但是不一定是我想要的結(jié)果,所以這個時候的模糊查詢框的update就是低優(yōu)先級,“老人”對應(yīng)UI的update相對input的update,優(yōu)先級就會低一些。在現(xiàn)在React18中,這個模糊查詢相關(guān)的UI可以被當(dāng)做transition。關(guān)于transition,等下我會有細(xì)講。

          4c22589a86bd67107c96facb869b8004.webp


          關(guān)于狀態(tài)復(fù)用

          Concurrent模式下,還支持狀態(tài)的復(fù)用。某些情況下,比如用戶走了,又回來,那么上一次的頁面狀態(tài)應(yīng)當(dāng)被保存下來,而不是完全從頭再來。當(dāng)然實際情況下不能緩存所有的頁面,不然內(nèi)存不得爆炸,所以還得做成可選的。目前,React正在用Offscreen組件來實現(xiàn)這個功能。嗯,也就是這關(guān)于這個狀態(tài)復(fù)用,其實還沒完成呢。不過源碼中已經(jīng)在做了:

          64d475b8ba6f3672a0b7c6f9ecd1d088.webp

          另外,使用OffScreen,除了可以復(fù)用原先的狀態(tài),我們也可以使用它來當(dāng)做新UI的緩存準(zhǔn)備,就是雖然新UI還沒登場,但是可以先在后臺準(zhǔn)備著嘛,這樣一旦輪到它,就可以立馬快速地渲染出來。

          Concurrent總結(jié)

          總結(jié)一下,Concurrent并不是API之類的新特性,但是呢,它很重要,因為它是React18大部分新特性的實現(xiàn)基礎(chǔ),包括Suspense、transitions、流式服務(wù)端渲染等。

          三、React的新特性

          前文說了那么多Concurrent并不是新特性,而是React18新特性的實現(xiàn)基礎(chǔ)。那么新特性都有哪些呢,下面來看吧:

          react-dom/client中的createRoot

          創(chuàng)建一個初次渲染或者更新,以前我們用的是ReactDOM.render,現(xiàn)在改用react-dom/client中的createRoot,這個函數(shù)的返回值是卸載函數(shù)。


          ssr中的ReactDOM.hydrate也換成了新的hydrateRoot。


          以上兩個API目前依然支持,只是已經(jīng)移入legacy模式,開發(fā)環(huán)境下會報warning。

          自動批量處理 Automatic Batching

          如果你是React技術(shù)棧,那么你一定遇到過無數(shù)次這樣的面試題:

          6836b79bf46fba98256d56bdbb168dd3.webp


          恭喜你,接下來React18之后,這個面試題中的前半部分可以被劃入史冊了,但是后半部分依然是你我React技術(shù)黨逃不開的宿命。不過也不是什么大事,誰讓你認(rèn)識我呢~
          先回答上面那個問題,可同步可異步,同步的話把setState放在promises、setTimeout或者原生事件中等。所謂異步就是個批量處理,為什么要批量處理呢。舉個例子,老人以打漁為生,難道要每打到一條沙丁魚就下船去集市上賣掉嗎,那跑來跑去的成本太高了,賣魚的錢都不夠路費的。所以老人都是打到魚之后先放到船艙,一段時間之后再跑一次集市,批量賣掉那些魚。對于React來說,也是這樣,state攢夠了再一起更新嘛。


          但是以前的React的批量更新是依賴于合成事件的,到了React18之后,state的批量更新不再與合成事件有直接關(guān)系,而是自動批量處理。
          //?以前:?這里的兩次setState并沒有批量處理,React會render兩次
          setTimeout(()?=>?{
          ??setCount(c?=>?c?+?1);
          ??setFlag(f?=>?!f);
          },?1000);

          //?React18:?自動批量處理,這里只會render一次
          setTimeout(()?=>?{
          ??setCount(c?=>?c?+?1);
          ??setFlag(f?=>?!f);
          },?1000);

          所以如果你項目中還在用setTimeout之列的“黑科技”實現(xiàn)setState的同步的話,升級React18之前,記得改一下~


          雖然建議setState批量處理,但是如果你有一些其它理由或者需要應(yīng)急,想要同步setState,這個時候可以使用flushSync,下面的例子中,log的count將會和button上的count同步:
          ???//?import?{?flushSync?}?from?"react-dom";
          ???changeCount?=?()?=>?{
          ????const?{?count?}?=?this.state;

          ????flushSync(()?=>?{
          ??????this.setState({
          ????????count:?count?+?1,
          ??????});
          ????});

          ????console.log("改變count",?this.state.count);?//sy-log
          ??};
          ??
          ??//?change?count?合成事件

          transition

          React把update分成兩種:

          • Urgent updates?緊急更新,指直接交互,通常指的用戶交互。如點擊、輸入等。這種更新一旦不及時,用戶就會覺得哪里不對。

          • Transition updates?過渡更新,如UI從一個視圖向另一個視圖的更新。通常這種更新用戶并不著急看到。


          startTransition

          startTransition可以用在任何你想更新的時候。但是從實際來說,以下是兩種典型適用場景:

          • 渲染慢:如果你有很多沒那么著急的內(nèi)容要渲染更新。

          • 網(wǎng)絡(luò)慢:如果你的更新需要花較多時間從服務(wù)端獲取。這個時候也可以再結(jié)合Suspense

          import?{useEffect,?useState,?Suspense}?from?"react";
          import?Button?from?"../components/Button";
          import?User?from?"../components/User";
          import?Num?from?"../components/Num";
          import?{fetchData}?from?"../utils";

          const?initialResource?=?fetchData();

          export?default?function?TransitionPage(props)?{
          ??const?[resource,?setResource]?=?useState(initialResource);

          ??//?useEffect(()?=>?{
          ??//???console.log("resource",?resource);?//sy-log
          ??//?},?[resource]);

          ??return?(
          ????<div>
          ??????<h3>TransitionPageh3>

          ??????<Suspense?fallback={<h1>loading?-?userh1>}>
          ????????<User?resource={resource}?/>
          ??????Suspense>

          ??????<Suspense?fallback={<h1>loading-numh1>}>
          ????????<Num?resource={resource}?/>
          ??????Suspense>

          ??????<Button
          ????????refresh={()?=>
          ?{
          ??????????setResource(fetchData());
          ????????}}
          ??????/>
          ????div>
          ??);
          }


          Button

          import?{
          ??//startTransition,
          ??useTransition,
          }?from?"react";

          export?default?function?Button({refresh})?{
          ??const?[isPending,?startTransition]?=?useTransition();

          ??return?(
          ????<div?className="border">
          ??????<h3>Buttonh3>

          ??????<button
          ????????onClick={()?=>
          ?{
          ??????????startTransition(()?=>?{
          ????????????refresh();
          ??????????});
          ????????}}
          ????????disabled={isPending}>
          ????????點擊刷新數(shù)據(jù)
          ??????button>
          ??????{isPending???<div>loading...div>?:?null}
          ????div>
          ??);
          }

          與setTimeout異同

          startTransition出現(xiàn)之前,我們可以使用setTimeout來實現(xiàn)優(yōu)化。但是現(xiàn)在在處理上面的優(yōu)化的時候,有了startTransition基本上可以拋棄setTimeout了,原因主要有以三點:首先,與setTimeout不同的是,startTransition并不會延遲調(diào)度,而是會立即執(zhí)行,startTransition接收的函數(shù)是同步執(zhí)行的,只是這個update被加了一個“transitions"的標(biāo)記。而這個標(biāo)記,React內(nèi)部處理更新的時候是會作為參考信息的。這就意味著,相比于setTimeout, 把一個update交給startTransition能夠更早地被處理。而在于較快的設(shè)備上,這個過度是用戶感知不到的。

          useTransition

          在使用startTransition更新狀態(tài)的時候,用戶可能想要知道transition的實時情況,這個時候可以使用React提供的hook api?useTransition

          import?{?useTransition?}?from?'react';
          const?[isPending,?startTransition]?=?useTransition();

          如果transition未完成,isPending值為true,否則為false。


          useDeferredValue

          使得我們可以延遲更新某個不那么重要的部分。

          相當(dāng)于參數(shù)版的transitions。

          舉例:如下圖,當(dāng)用戶在輸入框輸入“書”的時候,用戶應(yīng)該立馬看到輸入框的反應(yīng),而相比之下,下面的模糊查詢框如果延遲出現(xiàn)一會兒其實是完全可以接受的,因為用戶可能會繼續(xù)修改輸入框內(nèi)容,這個過程中模糊查詢結(jié)果還是會變化,但是這個變化對用戶來說相對沒那么重要,用戶最關(guān)心的是看到最后的匹配結(jié)果。


          82efe340cc847195cd0c4136a635bf8a.webp

          用法如下:

          import?{useDeferredValue,?useState}?from?"react";
          import?MySlowList?from?"../components/MySlowList";

          export?default?function?UseDeferredValuePage(props)?{
          ??const?[text,?setText]?=?useState("hello");
          ??const?deferredText?=?useDeferredValue(text);

          ??const?handleChange?=?(e)?=>?{
          ????setText(e.target.value);
          ??};
          ??return?(
          ????<div>
          ??????<h3>UseDeferredValuePageh3>

          ??????{/*?保持將當(dāng)前文本傳遞給?input?*/}
          ??????<input?value={text}?onChange={handleChange}?/>
          ??????{/*?但在必要時可以將列表“延后”?*/}
          ??????<p>{deferredText}p>

          ??????<MySlowList?text={deferredText}?/>
          ????div>
          ??);
          }

          MySlowList

          import?React,?{memo}?from?"react";

          function?ListItem({children})?{
          ??let?now?=?performance.now();
          ??while?(performance.now()?-?now?3)?{}
          ??return?<div?className="ListItem">{children}div>;
          }

          export?default?memo(function?MySlowList({text})?{
          ??let?items?=?[];
          ??for?(let?i?=?0;?i?80;?i++)?{
          ????items.push(
          ??????<ListItem?key={i}>
          ????????Result?#{i}?for?"{text}"
          ??????ListItem>

          ????);
          ??}
          ??return?(
          ????<div?className="border">
          ??????<p>
          ????????<b>Results?for?"{text}":b>

          ??????p>
          ??????<ul?className="List">{items}ul>
          ????div>
          ??);
          });

          Suspense

          可以“等待”目標(biāo)UI加載,并且可以直接指定一個加載的界面(像是個 spinner),讓它在用戶等待的時候顯示。
          }>
          ??<Comments?/>
          </Suspense>
          其實Suspense也早就出現(xiàn)在React中了,只不過之前功能有限。在React18中,背靠Concurrent模式,Suspense終于爆發(fā)了自己的光彩。


          在概念上,Suspense有點像catch,只不過Suspense捕獲的不是異常,而是組件的suspending狀態(tài),即掛載中。


          基本使用:避免等待太久

          import?{useState,?Suspense}?from?"react";
          import?User?from?"../components/User";
          import?Num?from?"../components/Num";
          import?{fetchData}?from?"../utils";
          import?ErrorBoundaryPage?from?"./ErrorBoundaryPage";

          const?initialResource?=?fetchData();

          export?default?function?SuspensePage(props)?{
          ??const?[resource,?setResource]?=?useState(initialResource);

          ??return?(
          ????<div>
          ??????<h3>SuspensePageh3>

          ??????<ErrorBoundaryPage?fallback={<h1>網(wǎng)絡(luò)出錯了h1>}>
          ????????<Suspense?fallback={<h1>loading?-?userh1>}>
          ??????????<User?resource={resource}?/>
          ????????Suspense>
          ??????ErrorBoundaryPage>

          ??????<Suspense?fallback={<h1>loading-numh1>}>
          ????????<Num?resource={resource}?/>
          ??????Suspense>

          ??????<button?onClick={()?=>?setResource(fetchData())}>refreshbutton>
          ????div>
          ??);
          }

          錯誤處理

          每當(dāng)使用 Promises,大概率我們會用?catch()?來做錯誤處理。但當(dāng)我們用 Suspense 時,我們不等待?Promises 就直接開始渲染,這時?catch()?就不適用了。這種情況下,錯誤處理該怎么進(jìn)行呢?在 Suspense 中,獲取數(shù)據(jù)時拋出的錯誤和組件渲染時的報錯處理方式一樣——你可以在需要的層級渲染一個錯誤邊界組件來“捕捉”層級下面的所有的報錯信息。
          export?default?class?ErrorBoundaryPage?extends?React.Component?{
          ??state?=?{hasError:?false,?error:?null};
          ??static?getDerivedStateFromError(error)?{
          ????return?{
          ??????hasError:?true,
          ??????error,
          ????};
          ??}
          ??render()?{
          ????if?(this.state.hasError)?{
          ??????return?this.props.fallback;
          ????}
          ????return?this.props.children;
          ??}
          }


          結(jié)合transitions

          所謂提高用戶體驗,一個重要的準(zhǔn)則就是保證UI的連續(xù)性,如下面的例子,如果此時我想把tab從‘photos’切換到‘comments’,但是Comments又沒法立馬渲染出來,這個時候不可避免地,就會Photos頁面消失,顯現(xiàn)Spinner的loading頁面,等一會兒,Comments頁面才姍姍來遲。
          function?handleClick()?{
          ??setTab('comments');
          }

          }>
          ??{tab?===?'photos'???<Photos?/>?:?<Comments?/>}
          </Suspense>
          從UI連續(xù)性上來說,這個中間出現(xiàn)的Spinner就已經(jīng)破壞了連續(xù)性。而實際上,正常人的反應(yīng)其實是沒有那么快,短暫的延遲我們是感覺不到的。所以考慮到UI的連續(xù)性,上面的例子,交互可不可以修改一下,把上面頁面的切換當(dāng)做transitions,這樣即使tab切換,但是依然短暫停留在Photos,之后再改變到

          Comments:

          function?handleClick()?{
          ??startTransition(()?=>?{
          ????setTab('comments');
          ??});
          }


          上面這個例子我們使用的是startTransition,如果需要知道pending狀態(tài),可以使用useTransition:
          const?[isPending,?startTransition]?=?useTransition();

          function?handleClick()?{
          ??startTransition(()?=>?{
          ????setTab('comments');
          ??});
          }

          }>
          ??<div?style={{?opacity:?isPending???0.8?:?1?}}>
          ????{tab?===?'photos'???<Photos?/>?:?<Comments?/>}
          ??div>

          </Suspense>

          SuspenseList

          用于控制Suspense組件的顯示順序。

          revealOrder?Suspense加載順序

          together?所有Suspense一起顯示,也就是最后一個加載完了才一起顯示全部

          forwards?按照順序顯示Suspense

          backwards?反序顯示Suspense

          tail是否顯示fallback,只在revealOrder為forwards或者backwards時候有效

          hidden不顯示

          collapsed輪到自己再顯示

          import?{useState,?Suspense,?SuspenseList}?from?"react";
          import?User?from?"../components/User";
          import?Num?from?"../components/Num";
          import?{fetchData}?from?"../utils";
          import?ErrorBoundaryPage?from?"./ErrorBoundaryPage";

          const?initialResource?=?fetchData();

          export?default?function?SuspenseListPage(props)?{
          ??const?[resource,?setResource]?=?useState(initialResource);

          ??return?(
          ????<div>
          ??????<h3>SuspenseListPageh3>

          ??????<SuspenseList?tail="collapsed">
          ????????<ErrorBoundaryPage?fallback={<h1>網(wǎng)絡(luò)出錯了h1>}>
          ??????????<Suspense?fallback={<h1>loading?-?userh1>}>
          ????????????<User?resource={resource}?/>
          ??????????Suspense>
          ????????ErrorBoundaryPage>

          ????????<Suspense?fallback={<h1>loading-numh1>}>
          ??????????<Num?resource={resource}?/>
          ????????Suspense>
          ??????SuspenseList>

          ??????<button?onClick={()?=>?setResource(fetchData())}>refreshbutton>
          ????div>
          ??);
          }

          四、新的Hooks

          8105cf7a084bd04f9cd5d3cb8066c7fd.webp


          關(guān)于useTransition與useDeferredValue上面已經(jīng)介紹過了,接下來說下React18其它的新Hooks,其中useSyncExternalStore與useInsertionEffect屬于Library Hooks。也就是普通應(yīng)用開發(fā)者一般用不到,這倆主要用于那些需要深度融合React模型的庫開發(fā),比如Recoil等。

          useId

          用于產(chǎn)生一個在服務(wù)端與Web端都穩(wěn)定且唯一的ID,也支持加前綴,這個特性多用于支持ssr的環(huán)境下:

          export?default?function?NewHookApi(props)?{
          ??const?id?=?useId();

          ??return?(
          ????<div>
          ??????<h3?id={id}>NewHookApih3>

          ????div>
          ??);
          }

          注意:useId產(chǎn)生的ID不支持css選擇器,如querySelectorAll。

          useSyncExternalStore

          const?state?=?useSyncExternalStore(subscribe,?getSnapshot[,?getServerSnapshot]);

          此Hook用于外部數(shù)據(jù)的讀取與訂閱,可應(yīng)用Concurrent。

          基本用法如下:

          import?{?useStore?}?from?"../store";
          import?{?useId,?useSyncExternalStore?}?from?"../whichReact";

          export?default?function?NewHookApi(props)?{
          ??const?store?=?useStore();
          ??const?state?=?useSyncExternalStore(store.subscribe,?store.getSnapshot);

          ??return?(
          ????<div>
          ??????<h3>NewHookApih3>


          ??????<button?onClick={()?=>?store.dispatch({?type:?"ADD"?})}>{state}button>
          ????div>
          ??);
          }

          useStore是我另外定義的,

          export?function?useStore()?{
          ??const?storeRef?=?useRef();

          ??if?(!storeRef.current)?{
          ????storeRef.current?=?createStore(countReducer);
          ??}

          ??return?storeRef.current;
          }

          function?countReducer(action,?state?=?0)?{
          ??switch?(action.type)?{
          ????case?"ADD":
          ??????return?state?+?1;
          ????case?"MINUS":
          ??????return?state?-?1;
          ????default:
          ??????return?state;
          ??}
          }

          這里的createStore用的redux思路:

          export?function?createStore(reducer)?{
          ??let?currentState;
          ??let?listeners?=?[];

          ??function?getSnapshot()?{
          ????return?currentState;
          ??}

          ??function?dispatch(action)?{
          ????currentState?=?reducer(action,?currentState);
          ????listeners.map((listener)?=>?listener());
          ??}

          ??function?subscribe(listener)?{
          ????listeners.push(listener);

          ????return?()?=>?{
          ??????//???console.log("unmount",?listeners);
          ????};
          ??}

          ??dispatch({?type:?"TIANNA"?});

          ??return?{
          ????getSnapshot,
          ????dispatch,
          ????subscribe,
          ??};
          }

          對于還在用自定義store來做低代碼項目的我有點開心,可以用于升級我的項目了,原先定義的forceUpdate、unsubscribe之類的,可以去掉了~

          useInsertionEffect

          useInsertionEffect(didUpdate);

          函數(shù)簽名同useEffect,但是它是在所有DOM變更前同步觸發(fā)。主要用于css-in-js庫,往DOM中動態(tài)注入

          <del id="afajh"><form id="afajh"></form></del>
              1. <th id="afajh"><progress id="afajh"></progress></th>
                <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美区亚洲区 | 日韩电影久久 | 骚逼影院′ | 麻豆国产视频 | 俺也去在线视频 |