React diff算法原理詳細(xì)分析
拋磚引玉
React通過引入Virtual DOM的概念,極大地避免無效的Dom操作,已使我們的頁面的構(gòu)建效率提到了極大的提升。但是如何高效地通過對比新舊Virtual DOM來找出真正的Dom變化之處同樣也決定著頁面的性能,React用其特殊的diff算法解決這個問題。Virtual DOM+React diff的組合極大地保障了React的性能,使其在業(yè)界有著不錯的性能口碑。diff算法并非React首創(chuàng),React只是對diff算法做了一個優(yōu)化,但卻是因為這個優(yōu)化,給React帶來了極大的性能提升,不禁讓人感嘆React創(chuàng)造者們的智慧!接下來我們就探究一下React的diff算法。
傳統(tǒng)diff算法
在文章開頭我們提到React的diff算法給React帶來了極大的性能提升,而之前的React diff算法是在傳統(tǒng)diff算法上的優(yōu)化。下面我們先看一下傳統(tǒng)的diff算法是什么樣子的。
傳統(tǒng)diff算法通過循環(huán)遞歸對節(jié)點進行依次對比,效率低下,算法復(fù)雜度達(dá)到 O(n^3),其中 n 是樹中節(jié)點的總數(shù)。
O(n^3) 到底有多可怕呢?這意味著如果要展示 1000 個節(jié)點,就要依次執(zhí)行上十億次 的比較,這種指數(shù)型的性能消耗對于前端渲染場景來說代價太高了。而React卻這個diff算法時間復(fù)雜度從O(n3)降到O(n)。O(n3)到O(n)的提升有多大,我們通過一張圖來看一下。
從上面這張圖來看,React的diff算法所帶來的提升無疑是巨大無比的。接下來我們再看一張圖:
從1979到2011,30多年的時間,才將時間復(fù)雜度搞到O(n^3),而React從開源到現(xiàn)在不過區(qū)區(qū)幾年的時間,卻一下子干到O(n),到這里不禁再次膜拜一下React的創(chuàng)造者們。
那么React這個diff算法是如何做到的呢?
React diff原理
前面我們講到傳統(tǒng)diff算法的時間復(fù)雜度為O(n3),其中n為樹中節(jié)點的總數(shù),隨著n的增加,diff所耗費的時間將呈現(xiàn)爆炸性的增長。react卻利用其特殊的diff算法做到了O(n3)到O(n)的飛躍性的提升,而完成這一壯舉的法寶就是下面這三條看似簡單的diff策略:
- Web UI中DOM節(jié)點跨層級的移動操作特別少,可以忽略不計。
- 擁有相同類的兩個組件將會生成相似的樹形結(jié)構(gòu),擁有不同類的兩個組件將會生成不同的樹形結(jié)構(gòu)。
- 對于同一層級的一組子節(jié)點,它們可以通過唯一 id 進行區(qū)分。
在上面三個策略的基礎(chǔ)上,React 分別將對應(yīng)的tree diff、component diff 以及 element diff 進行算法優(yōu)化,極大地提升了diff效率。
tree diff
基于策略一,React 對樹的算法進行了簡潔明了的優(yōu)化,即對樹進行分層比較,兩棵樹只會對同一層次的節(jié)點進行比較。
既然 DOM 節(jié)點跨層級的移動操作少到可以忽略不計,針對這一現(xiàn)象,React只會對相同層級的 DOM 節(jié)點進行比較,即同一個父節(jié)點下的所有子節(jié)點。當(dāng)發(fā)現(xiàn)節(jié)點已經(jīng)不存在時,則該節(jié)點及其子節(jié)點會被完全刪除掉,不會用于進一步的比較。這樣只需要對樹進行一次遍歷,便能完成整個 DOM 樹的比較。
策略一的前提是Web UI中DOM節(jié)點跨層級的移動操作特別少,但并沒有否定DOM節(jié)點跨層級的操作的存在,那么當(dāng)遇到這種操作時,React是如何處理的呢?
接下來我們通過一張圖來展示整個處理過程:
A 節(jié)點(包括其子節(jié)點)整個被移動到 D 節(jié)點下,由于 React 只會簡單地考慮同層級節(jié)點的位置變換,而對于不 同層級的節(jié)點,只有創(chuàng)建和刪除操作。當(dāng)根節(jié)點發(fā)現(xiàn)子節(jié)點中 A 消失了,就會直接銷毀 A;當(dāng) D 發(fā)現(xiàn)多了一個子節(jié)點 A,則會創(chuàng) 建新的 A(包括子節(jié)點)作為其子節(jié)點。此時,diff 的執(zhí)行情況:create A → create B → create C → delete A。
由此可以發(fā)現(xiàn),當(dāng)出現(xiàn)節(jié)點跨層級移動時,并不會出現(xiàn)想象中的移動操作,而是以 A 為根節(jié)點的整個樹被重新創(chuàng)建。這是一種影響React性能的操作,因此官方建議不要進行 DOM 節(jié)點跨層級的操作。
在開發(fā)組件時,保持穩(wěn)定的 DOM 結(jié)構(gòu)會有助于性能的提升。例如,可以通過 CSS 隱藏或顯示節(jié)點,而不是真正地移
除或添加 DOM 節(jié)點。
component diff
React 是基于組件構(gòu)建應(yīng)用的,對于組件間的比較所采取的策略也是非常簡潔、高效的。
如果是同一類型的組件,按照原策略繼續(xù)比較 Virtual DOM 樹即可。
如果不是,則將該組件判斷為 dirty component,從而替換整個組件下的所有子節(jié)點。
對于同一類型的組件,有可能其 Virtual DOM 沒有任何變化,如果能夠確切知道這點,那么就可以節(jié)省大量的 diff 運算時間。因此,React允許用戶通過shouldComponentUpdate()來判斷該組件是否需進行diff算法分析,但是如果調(diào)用了forceUpdate方法,shouldComponentUpdate則失效。
參考React實戰(zhàn)視頻講解:進入學(xué)習(xí)
接下來我們看下面這個例子是如何實現(xiàn)轉(zhuǎn)換的:
轉(zhuǎn)換流程如下:
當(dāng)組件D變?yōu)榻M件G時,即使這兩個組件結(jié)構(gòu)相似,一旦React判斷D和G是不同類型的組件,就不會比較二 者的結(jié)構(gòu),而是直接刪除組件D,重新創(chuàng)建組件G及其子節(jié)點。雖然當(dāng)兩個組件是不同類型但結(jié)構(gòu)相似時,diff會影響性能,但正如React官方博客所言:不同類型的組件很少存在相似DOM樹的情況,因此這種極端因素很難在實際開發(fā)過程中造成重大的影響。
element diff
當(dāng)節(jié)點處于同一層級時,diff 提供了 3 種節(jié)點操作,分別為 INSERT_MARKUP (插入)、MOVE_EXISTING (移動)和 REMOVE_NODE (刪除)。
INSERT_MARKUP :新的組件類型不在舊集合里,即全新的節(jié)點,需要對新節(jié)點執(zhí)行插入操作。
MOVE_EXISTING :舊集合中有新組件類型,且 element 是可更新的類型,generateComponentChildren 已調(diào)用
receiveComponent ,這種情況下 prevChild=nextChild ,就需要做移動操作,可以復(fù)用以前的 DOM 節(jié)點。
REMOVE_NODE :舊組件類型,在新集合里也有,但對應(yīng)的 element 不同則不能直接復(fù)用和更新,需要執(zhí)行刪除操作,或者舊組件不在新集合里的,也需要執(zhí)行刪除操作。
舊集合中包含節(jié)點A、B、C和D,更新后的新集合中包含節(jié)點B、A、D和C,此時新舊集合進行diff差異化對比,發(fā)現(xiàn)B!=A,則創(chuàng)建并插入B至新集合,刪除舊集合A;以此類推,創(chuàng)建并插入A、D和C,刪除B、C和D。
我們發(fā)現(xiàn)這些都是相同的節(jié)點,僅僅是位置發(fā)生了變化,但卻需要進行繁雜低效的刪除、創(chuàng)建操作,其實只要對這些節(jié)點進行位置移動即可。React針對這一現(xiàn)象提出了一種優(yōu)化策略:允許開發(fā)者對同一層級的同組子節(jié)點,添加唯一 key 進行區(qū)分。 雖然只是小小的改動,性能上卻發(fā)生了翻天覆地的變化!我們再來看一下應(yīng)用了這個策略之后,react diff是如何操作的。
通過key可以準(zhǔn)確地發(fā)現(xiàn)新舊集合中的節(jié)點都是相同的節(jié)點,因此無需進行節(jié)點刪除和創(chuàng)建,只需要將舊集合中節(jié)點的位置進行移動,更新為新集合中節(jié)點的位置,此時React 給出的diff結(jié)果為:B、D不做任何操作,A、C進行移動操作即可。
具體的流程我們用一張表格來展現(xiàn)一下:
index | 節(jié)點 | oldIndex | maxIndex | 操作 |
---|---|---|---|---|
0 | B | 1 | 0 | oldIndex(1)>maxIndex(0),maxIndex=oldIndex,maxIndex變?yōu)? |
1 | A | 0 | 1 | oldIndex(0)<maxIndex(1),節(jié)點A移動至index(1)的位置 |
2 | D | 3 | 1 | oldIndex(3)>maxIndex(1),maxIndex=oldIndex,maxIndex變?yōu)? |
3 | C | 2 | 3 | oldIndex(2)<maxIndex(3),節(jié)點C移動至index(3)的位置 |
- index: 新集合的遍歷下標(biāo)。
- oldIndex:當(dāng)前節(jié)點在老集合中的下標(biāo)。
- maxIndex:在新集合訪問過的節(jié)點中,其在老集合的最大下標(biāo)值。
操作一欄中只比較oldIndex和maxIndex:
- 當(dāng)oldIndex>maxIndex時,將oldIndex的值賦值給maxIndex
- 當(dāng)oldIndex=maxIndex時,不操作
- 當(dāng)oldIndex<maxIndex時,將當(dāng)前節(jié)點移動到index的位置
上面的例子僅僅是在新舊集合中的節(jié)點都是相同的節(jié)點的情況下,那如果新集合中有新加入的節(jié)點且舊集合存在 需要刪除的節(jié)點,那么 diff 又是如何對比運作的呢?
index | 節(jié)點 | oldIndex | maxIndex | 操作 |
---|---|---|---|---|
0 | B | 1 | 0 | oldIndex(1)>maxIndex(0),maxIndex=oldIndex,maxIndex變?yōu)? |
1 | E | - | 1 | oldIndex不存在,添加節(jié)點E至index(1)的位置 |
2 | C | 2 | 1 | oldIndex(2)>maxIndex(1),maxIndex=oldIndex,maxIndex變?yōu)? |
3 | A | 0 | 2 | oldIndex(0)<maxIndex(2),節(jié)點A移動至index(3)的位置 |
注:最后還需要對舊集合進行循環(huán)遍歷,找出新集合中沒有的節(jié)點,此時發(fā)現(xiàn)存在這樣的節(jié)點D,因此刪除節(jié)點D,到此 diff 操作全部完成。
同樣操作一欄中只比較oldIndex和maxIndex,但是oldIndex可能有不存在的情況:
oldIndex存在
- 當(dāng)oldIndex>maxIndex時,將oldIndex的值賦值給maxIndex
- 當(dāng)oldIndex=maxIndex時,不操作
- 當(dāng)oldIndex<maxIndex時,將當(dāng)前節(jié)點移動到index的位置
oldIndex不存在
新增當(dāng)前節(jié)點至index的位置
當(dāng)然這種diff并非完美無缺的,我們來看這么一種情況:
實際我們只需對D執(zhí)行移動操作,然而由于D在舊集合中的位置是最大的,導(dǎo)致其他節(jié)點的oldIndex < maxIndex,造成D沒有執(zhí)行移動操作,而是A、B、C全部移動到D節(jié)點后面的現(xiàn)象。針對這種情況,官方建議:
在開發(fā)過程中,盡量減少類似將最后一個節(jié)點移動到列表首部的操作。當(dāng)節(jié)點數(shù)量過大或更新操作過于頻繁時,這在一定程度上會影響React的渲染性能。
由于key的存在,react可以準(zhǔn)確地判斷出該節(jié)點在新集合中是否存在,這極大地提高了diff效率。我們在開發(fā)過中進行列表渲染的時候,若沒有加key,react會拋出警告要求開發(fā)者加上key,就是為了提高diff效率。但是加了key一定要比沒加key的性能更高嗎?我們再來看一個例子:
現(xiàn)在有一集合[1,2,3,4,5],渲染成如下的樣子:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
---------------
現(xiàn)在我們將這個集合的順序打亂變成[1,3,2,5,4]。
1.加key
<div key='1'>1</div> <div key='1'>1</div>
<div key='2'>2</div> <div key='3'>3</div>
<div key='3'>3</div> ========> <div key='2'>2</div>
<div key='4'>4</div> <div key='5'>5</div>
<div key='5'>5</div> <div key='4'>4</div>
操作:節(jié)點2移動至下標(biāo)為2的位置,節(jié)點4移動至下標(biāo)為4的位置。2.不加key
<div>1</div> <div>1</div>
<div>2</div> <div>3</div>
<div>3</div> ========> <div>2</div>
<div>4</div> <div>5</div>
<div>5</div> <div>4</div>
操作:修改第1個到第5個節(jié)點的innerText
---------------
如果我們對這個集合進行增刪的操作改成[1,3,2,5,6]。
1.加key
<div key='1'>1</div> <div key='1'>1</div>
<div key='2'>2</div> <div key='3'>3</div>
<div key='3'>3</div> ========> <div key='2'>2</div>
<div key='4'>4</div> <div key='5'>5</div>
<div key='5'>5</div> <div key='6'>6</div>
操作:節(jié)點2移動至下標(biāo)為2的位置,新增節(jié)點6至下標(biāo)為4的位置,刪除節(jié)點4。2.不加key
<div>1</div> <div>1</div>
<div>2</div> <div>3</div>
<div>3</div> ========> <div>2</div>
<div>4</div> <div>5</div>
<div>5</div> <div>6</div>
操作:修改第1個到第5個節(jié)點的innerText
---------------
通過上面這兩個例子我們發(fā)現(xiàn):
由于dom節(jié)點的移動操作開銷是比較昂貴的,沒有key的情況下要比有key的性能更好。
通過上面的例子我們發(fā)現(xiàn),雖然加了key提高了diff效率,但是未必一定提升了頁面的性能。因此我們要注意這么一點:
對于簡單列表頁渲染來說,不加key要比加了key的性能更好
根據(jù)上面的情況,最后我們總結(jié)一下key的作用:
- 準(zhǔn)確判斷出當(dāng)前節(jié)點是否在舊集合中
- 極大地減少遍歷次數(shù)
應(yīng)用實踐
頁面指定區(qū)域刷新
現(xiàn)在有這么一個需求,當(dāng)用戶身份變化時,當(dāng)前頁面重新加載數(shù)據(jù)。猛一看過去覺得非常簡單,沒啥難度的,只要在componentDidUpdate這個生命周期里去判斷用戶身份是否發(fā)生改變,如果發(fā)生改變就重新請求數(shù)據(jù),于是就有了以下這一段代碼:
import React from 'react'; import {connect} from 'react-redux'; let oldAuthType = '';//用來存儲舊的用戶身份 @connect( state=>state.user ) class Page1 extends React.PureComponent{ state={ loading:true } loadMainData(){ //這里采用了定時器去模擬數(shù)據(jù)請求 this.setState({ loading:true }); const timer = setTimeout(()=>{ this.setState({ loading:false }); clearTimeout(timer); },2000); } componentDidUpdate(){ const {authType} = this.props; //判斷當(dāng)前用戶身份是否發(fā)生了改變 if(authType!==oldAuthType){ //存儲新的用戶身份 oldAuthType=authType; //重新加載數(shù)據(jù) this.loadMainData(); } } componentDidMount(){ oldAuthType=this.props.authType; this.loadMainData(); } render(){ const {loading} = this.state; return ( <h2>{`頁面1${loading?'加載中...':'加載完成'}`}</h2> ) } } export default Page1;
看上去我們僅僅通過加上一段代碼就完成了這一需求,但是當(dāng)我們頁面是幾十個的時候,那這種方法就顯得捉襟見肘了。哪有沒有一個很好的方法來實現(xiàn)這個需求呢?其實很簡單,利用react diff的特性就可以實現(xiàn)它。對于這個需求,實際上就是希望當(dāng)前組件可以銷毀在重新生成,那怎么才能讓其銷毀并重新生成呢?通過上面的總結(jié)我發(fā)現(xiàn)兩種情況,可以實現(xiàn)組件的銷毀并重新生成。
當(dāng)組件類型發(fā)生改變當(dāng)key值發(fā)生變化
接下來我們就結(jié)合這兩個特點,用兩種方法去實現(xiàn)。
第一種:引入一個loading組件。切換身份時設(shè)置loading為true,此時loading組件顯示;切換身份完成,loading變?yōu)閒alse,其子節(jié)點children顯示。
<div className="g-main">{<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->loading?<Loading/>:children}</div>
第二種:在刷新區(qū)域加上一個key值就可以了,用戶身份一改變,key值就發(fā)生改變。
<div className="g-main" key={authType}>{children}</div>
第一種和第二種取舍上,個人建議的是這樣子的:
如果需要請求服務(wù)器的,用第一種,因為請求服務(wù)器會有一定等待時間,加入loading組件可以讓用戶有感知,體驗更好。如果是不需要請求服務(wù)器的情況下,選用第二種,因為第二種更簡單實用。
更加方便地監(jiān)聽props改變
針對這個需求,我們喜歡將搜索條件封裝成一個組件,查詢列表封裝成一個組件。其中查詢列表會接收一個查詢參數(shù)的屬性,如下所示:
import React from 'react'; import {Card} from 'antd'; import Filter from './components/filter'; import Teacher from './components/teacher'; export default class Demo2 extends React.PureComponent{ state={ filters:{ name:undefined, height:undefined, age:undefined } } handleFilterChange=(filters)=>{ this.setState({ filters }); } render(){ const {filters} = this.state; return <Card> {/* 過濾器 */} <Filter onChange={this.handleFilterChange}/> {/* 查詢列表 */} <Teacher filters={filters}/> </Card> } }
現(xiàn)在我們面臨一個問題,如何在組件Teacher中監(jiān)聽filters的變化,由于filters是一個引用類型,想監(jiān)聽其變化變得有些復(fù)雜,好在lodash提供了比較兩個對象的工具方法,使其簡單了。但是如果后期給Teacher加了額外的props,此時你要監(jiān)聽多個props的變化時,你的代碼將變得比較難以維護。針對這個問題,我們依舊可以通過key值去實現(xiàn),當(dāng)每次搜索時,重新生成一個key,那么Teacher組件就會重新加載了。代碼如下:
import React from 'react'; import {Card} from 'antd'; import Filter from './components/filter'; import Teacher from './components/teacher'; export default class Demo2 extends React.PureComponent{ state={ filters:{ name:undefined, height:undefined, age:undefined }, tableKey:this.createTableKey() } createTableKey(){ return Math.random().toString(36).substring(7); } handleFilterChange=(filters)=>{ this.setState({ filters, //重新生成tableKey tableKey:this.createTableKey() }); } render(){ const {filters,tableKey} = this.state; return <Card> {/* 過濾器 */} <Filter onChange={this.handleFilterChange}/> {/* 查詢列表 */} <Teacher key={tableKey} filters={filters}/> </Card> } }
即使后期給Teacher加入新的props,也沒有問題,只需拼接一下key即可:
<Teacher key={`${tableKey}-${prop1}-${prop2}`} filters={filters} prop1={prop1} prop2={prop2}/>
react-router中Link問題
先看一下demo代碼:
import React from 'react'; import {Card,Spin,Divider,Row,Col} from 'antd'; import {Link} from 'react-router-dom'; const bookList = [{ bookId:'1', bookName:'三國演義', author:'羅貫中' },{ bookId:'2', bookName:'水滸傳', author:'施耐庵' }] export default class Demo3 extends React.PureComponent{ state={ bookList:[], bookId:'', loading:true } loadBookList(bookId){ this.setState({ loading:true }); const timer = setTimeout(()=>{ this.setState({ loading:false, bookId, bookList }); clearTimeout(timer); },2000); } componentDidMount(){ const {match} = this.props; const {params} = match; const {bookId} = params; this.loadBookList(bookId); } render(){ const {bookList,bookId,loading} = this.state; const selectedBook = bookList.find((book)=>book.bookId===bookId); return <Card> <Spin spinning={loading}> { selectedBook&&(<div> <img width="120" src={`/static/images/book_cover_${bookId}.jpeg`}/> <h4>書名:{selectedBook?selectedBook.bookName:'--'}</h4> <div>作者:{selectedBook?selectedBook.author:'--'}</div> </div>) } <Divider orientation="left">關(guān)聯(lián)圖書</Divider> <Row> { bookList.filter((book)=>book.bookId!==bookId).map((book)=>{ const {bookId,bookName} = book; return <Col span={6}> <img width="120" src={`/static/images/book_cover_${bookId}.jpeg`}/> <h4><Link to={`/demo3/${bookId}`}>{bookName}</Link></h4> </Col> }) } </Row> </Spin> </Card> } }
通過演示gif,我們看到了地址欄的地址已經(jīng)發(fā)生改變,但是并沒有我們想象中那樣從新走一遍componentDidMount去請求數(shù)據(jù),這說明我們的組件并沒有實現(xiàn)銷毀并重新生成這么一個過程。解決這個問題你可以在componentDidUpdate去監(jiān)聽其改變:
componentDidUpdate(){ const {match} = this.props; const {params} = match; const {bookId} = params; if(bookId!==this.state.bookId){ this.loadBookList(bookId); } }
前面我們說過如果是后期需要監(jiān)聽多個props的話,這樣子后期維護比較麻煩.同樣我們還是利用key去解決這個問題,首頁我們可以將頁面封裝成一個組件BookDetail,并且在其外層再包裹一層,再去給BookDetail加key,代碼如下:
import React from 'react'; import BookDetail from './bookDetail'; export default class Demo3 extends React.PureComponent{ render(){ const {match} = this.props; const {params} = match; const {bookId} = params; return <BookDetail key={bookId} bookId={bookId}/> } }
這樣的好處是我們代碼結(jié)構(gòu)更加清晰,后續(xù)拓展新功能比較簡單。
結(jié)語
React的高效得益于其Virtual DOM+React diff的體系。diff算法并非react獨創(chuàng),react只是在傳統(tǒng)diff算法做了優(yōu)化。但因為其優(yōu)化,將diff算法的時間復(fù)雜度一下子從O(n^3)降到O(n)。
React diff的三大策略:
- Web UI中DOM節(jié)點跨層級的移動操作特別少,可以忽略不計。
- 擁有相同類的兩個組件將會生成相似的樹形結(jié)構(gòu),擁有不同類的兩個組件將會生成不同的樹形結(jié)構(gòu)。
- 對于同一層級的一組子節(jié)點,它們可以通過唯一 id 進行區(qū)分。
在開發(fā)組件時,保持穩(wěn)定的 DOM 結(jié)構(gòu)會有助于性能的提升。
在開發(fā)過程中,盡量減少類似將最后一個節(jié)點移動到列表首部的操作。key的存在是為了提升diff效率,但未必一定就可以提升性能,記住簡單列表渲染情況下,不加key要比加key的性能更好。
懂得借助react diff的特性去解決我們實際開發(fā)中的一系列問題。
到此這篇關(guān)于React diff算法原理詳細(xì)分析的文章就介紹到這了,更多相關(guān)React diff算法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解在create-react-app使用less與antd按需加載
這篇文章主要介紹了詳解在create-react-app使用less與antd按需加載,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-12-12react中fetch之cors跨域請求的實現(xiàn)方法
本篇文章主要介紹了react中fetch之cors跨域請求的實現(xiàn)方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-03-03React前端渲染優(yōu)化--父組件導(dǎo)致子組件重復(fù)渲染的問題
本篇文章是針對父組件導(dǎo)致子組件重復(fù)渲染的優(yōu)化方法,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08React報錯之Object?is?possibly?null的問題及解決方法
這篇文章主要介紹了React報錯之Object?is?possibly?null的問題,造成 "Object is possibly null"的錯誤是因為useRef()鉤子可以傳遞一個初始值作為參數(shù),而我們傳遞null作為初始值,本文給大家分享詳細(xì)解決方法,需要的朋友可以參考下2022-07-07解決React報錯Unexpected default export of an
這篇文章主要為大家介紹了React報錯Unexpected default export of anonymous function解決方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12