close

前陣子剛完成了一個讀書會的導讀,內容與React component有關,加上新學校的新同學也有一個跟component相關的優化方式分享,就延伸出了很多我對於component的疑問,而這些疑問也透過讀書會在Zet大大的解答下,稍微有比較明白一些。說好像懂了,但是感覺還是需要更完整地整理過一次對這部分的理解才會更清楚,所以今天也就稍微整理一下那次讀書會自己學到的一些新東西,以及對component的一些理解吧!

什麼是component ?
原本我的認知是
當提到component,以前我的認知都會覺得就是一個component。沒錯!這句話聽起來就是個「廢話」。再更明確地說明我原本的認知的話,也就是原本我以為「component是一個已經變成畫面的某一塊小區塊」,例如畫面中渲染出來的一個表格,是一個表格的component。
但實際上是
產生特定React element的藍圖,或者也可以說是設計稿,也就是我們寫出來的那一個component function本身才是所謂的component。也就是說component只是個可以產出一個React element的底稿,是一個函式,而不是被產生出來的React element本身。

component和React element的差異是什麼?
剛剛已經提到React element了,對於component的定義如果還有點不太理解的人,可能還會有一個疑問,那就是「所以component和React element的差異是什麼?」。
React element是在React中建構畫面的最小單位,也是React內的virtual DOM,而component只是一個可以產生出React element的藍圖。但這裡必須補充一點,產生React Element的方式不只有component,單純用div、p這種標籤名稱一樣也可以創建React element。雖然這兩種方式:「component」和「實際DOM element名稱」都能夠創建React element,但是JSX在轉譯時,會以標籤首字母的大小寫來判斷這是一個實際的DOM element名稱還是component function的名稱。例如以下這樣:

【前端新手日記】React Component - <com

用component建立React element的方法&差異
既然今天的主角是component,那就來看一下要怎麼透過component來創建React element吧!
這裡先定義一個AComponent

【前端新手日記】React Component - <com

主要有兩種用法:
- 用標籤的寫法的呼叫
【前端新手日記】React Component - <com

- 直接呼叫component function
【前端新手日記】React Component - <com

差異
不管是哪種方法,我們實際使用時,都可以看到最後的畫面是這樣。那兩種寫法建立出來的React element的差異究竟是什麼呢?
【前端新手日記】React Component - <com

首先,我們先把這兩種寫法印出來看看。
【前端新手日記】React Component - <com

會發現這兩種寫法有一個很明顯的差異,當我們是標籤寫法時,它會一是一個component類型的React element;但是當我們透過呼叫函式的寫法使用時,它會直接變成單純的h2的React element內容,也就是它不是一個component類型的React element,而是直接獲得了這個component function回傳出來的h2 React element。


【前端新手日記】React Component - <com
我們也可以透過使用React Developer Tools來觀察差異!
當我們是標籤寫法時,在React Developer Tools上會標示出這個component類型的React element名稱,而不是單純只有h2。
【前端新手日記】React Component - <com
但是當我們透過呼叫函式的寫法使用時,在React Developer Tools上只會顯示h2。

【前端新手日記】React Component - <com

更進一步地說這兩種差異的話,也就是如果是component類型的React element的話,React會延遲呼叫這個component。關於這部分,可以透過console.log來進一步看看實際上的差異。

這裡先用<AComponent>的寫法,來看看實際上的呼叫順序,以及所謂的延遲呼叫是什麼意思。

【前端新手日記】React Component - <com
可以思考一下當這樣寫之後,console.log的順序是什麼?AComponent裡面有一個console.log('render child: AComponent'),父component裡面也有兩個console.log()。在這個狀況下,依照程式碼順序來看的話,都會覺得console.log的順序會應該會是:
‘render ParentComponent’
'render child: AComponent'
'before result return'

但是結果真的是這樣嗎?直接來看看真正的結果!!!
【前端新手日記】React Component - <com
想不到吧!第二個居然是先印出'before result return',而不是印出'render child: AComponent',這也就是前面提到的「React會延遲呼叫component」的部分。

接下來我們一樣回到用呼叫函式的方法,看看和現在這個方式,是否真的有差異。

【前端新手日記】React Component - <com

最後印出的順序就是我們原先預想的順序,原因就在於這個寫法,React並不會用延遲呼叫來做處理。
【前端新手日記】React Component - <com

為什麼能做到延遲呼叫?從React的畫面更新機制看延遲呼叫的處理
在知道兩種不同的寫法上,於實際上component的呼叫這部分有差異後,再來進一步了解為什麼這樣寫就能讓React進行延遲呼叫的處理。在這部分會以component進行畫面更新機制的流程,來看會什麼會有這樣的差異。

首先先明確定義等下提到的的「render」,是指什麼樣的動作。等下會提到的「render」並不是把實際畫面渲染出來的那種瀏覽器渲染,而是呼叫component,來建立React element,也就是創建Virtual DOM的動作。再來白話說一下畫面更新機制主要會經歷整什麼樣的過程:如果是第一次render,主要會是「呼叫component function產生 React element -> 檢查產生的React element -> 將產生的React element同步到實體的DOM」,如果是因為setState而進行的re-render,就會是「以新狀態為基礎呼叫component function,產生一個新的React element -> 檢查產生的React element,並比較新舊的React element -> 將比較出的差異處同步到實體的DOM」,其中比較新舊React element的過程就是所謂的Reconciliation。

大致上了解React畫面的更新機制後,我們再更近一步地來看這部分和延遲呼叫的關係是什麼?
當render的時候,會先產生React element,在這個階段碰到component類型的react element的時候(也就是當我們以<AComponent />這個寫法進行component的呼叫時),只會「記得這裡要放某個特定的React element」,還不會馬上呼叫component function來產生這個React element。在進一步做檢查React Element的動作時,當發現到有不是普通的React element的時候,才會去真正地呼叫這個Component function產生相應的React element。也就是說在父層的component function被呼叫產生React element的這個階段,並不會將component類型的react element也被創建出來,也就能讓子component被延遲呼叫。而有了延遲呼叫,也就讓還不需要真的顯示的部分,先不要被呼叫,以優化頁面的效能。

 

React畫面更新機制流程下可活用的效能優化方法
前面已經大致看了畫面更新機制的流程,也知道在這個流程中,React會針對component類型的React element進行延遲呼叫的處理,再來看一個跟畫面更新機制流程有關的一個可以使用的效能優化方式。這個部分是在新學校內辦的讀書會中,負責同學分享的內容,其實當下聽到時.心中有些疑問,會有疑問的地方不是對方法本身有疑問,而是對為什麼這個方法可以有這樣的效果有疑問,所以也剛好趁另一個校外參加的讀書會中,以component這個主題,一起了解了這個優化的小技巧。

首先看一個校內讀書會中書中提到的例子.這是一個可以滑動的畫面,可以看到右側有一個紅色的方塊,上面有標示數字,在當前的狀況下,當滑動的時候,可以感受到有點卡卡的。
【前端新手日記】React Component - <Com

我們再從程式碼了解到底是發生了什麼事情?
從程式碼可以發現到App裡面有position的state,當我們進行scroll的動作時,會去setPosition,也就是說當position一有變動,App就會re-render,而App底下的所有子component當然也都會re-render,到這裡看起來很正常也很合理。不過現在的問題出在App底下出現的子component剛好有幾個是內含非常複雜計算邏輯的component,也就是說當它們因為App re-render而造成它們也re-render時,那個複雜的邏輯也就會被重新被呼叫一次。但在這個情境中,scroll的position跟那些邏輯複雜的子component根本沒有關係,所以在這個狀況下,也就「讓子component出現沒有意義的re-render」的狀況。

【前端新手日記】React Component - <Com

那該怎麼解決這個問題?
我們可以利用React畫面更新機制中的reconciler的階段,也就是我們前面有提到的產出React Element後,檢查React element,並比較差異的階段,來改善這個狀況。更白話一點的說明如何在這個階段優化這樣的狀況的話,也就是「在re-render的時,產出React Element之後,會檢查新建立的React Element的內容,當React覺得新建立的React element中某一區塊的React Element指向的記憶體位置與舊React element中相對應區塊的React element指向的記憶體位址相同時,React就會認定這個區塊的React Element沒有變動,而不再進行這個區塊對應之component類型的React element的re-render」。我想大家應該都知道在reconciliation階段的時候,會比較新舊React element的結構是否相同,但其實並不單單只會比較結構,還會比較指向的記憶體位址。

如果要再更聚焦該怎麼解決這種因為多餘re-render產生的問題,那也就是「讓這個情況中,不需要進行多餘re-render的component在"state有變動,進而造成與state有關連的component進行re-render時",都還是指向同一個記憶體位址」,所以主要目標也就是「
讓React element一直指向同一個記憶體位址」。

讓React element一直維持指向同一個記憶體位置的話方式中,主要有這幾種:
- 透過props傳入component類型的React element(包含一般的prop或children prop)

【前端新手日記】React Component - <Com

- 將這個component類型的React element在父component的component function外宣告成另一個變數
【前端新手日記】React Component - <Com

- 透過memo把這個子component緩存起來
可以用React.memo包起來

【前端新手日記】React Component - <Com

【前端新手日記】React Component - <Com

也可以使用useMemo包一層,再放入要使用的位置。
【前端新手日記】React Component - <Com

這裡也再回過頭思考一下,為什麼這幾個方式都能讓我們不想要進行多餘re-render的部分,在父component re-render的時候,它們還維持指向同一個記憶體位置?主要是因為當setCount的時候,re-render的是這個父component,也就是範例程式碼中的ParentComponent,所以不論是透過props由外帶入,或是在component外層宣告變數再放入,這兩個方式處理的React element都不會再被重新宣告,自然指向的記憶體位址也就會是相同的。而透過memo的方式能維持指向相同的記憶體位址,主要是因為memo的機制就是會先進行緩存,當傳入這個memo緩存的React element的props有變動,或是相依值有變動時,才會進行re-render,否則會使用緩存下來的React element。不過由於memo的做法會多緩存和比較相依值的動作,所以還會需要React額外做一些動作,加上寫法比較不直覺,所以在一般情況下,其實透過props傳入來處理就好,不過單純以結果來看的話,props的做法或是memo的做法都是可行的。

最後回到一開始的範例中,是怎樣透過剛剛提到的目標「讓React element一直指向同一個記憶體位址」來解決滾動畫面造成的畫面卡卡問題。主要使用的方式也就是「
利用props來讓要使用的React element維持指向同一個記憶體位置」。
這裡先把和position有關聯的部分,整個抽出一個獨立的component。

【前端新手日記】React Component - <Com

再把耗效能的部分裝在一起,透過props帶進這個剛剛定義好的scroll的component中。
【前端新手日記】React Component - <Com

最後想要強調的是雖然在這個範例程式碼中的解法是透過props把耗效能的部分傳入scroll相關的component中,但並不是因為props本身帶有component re-render時,能夠略過重新被呼叫的特性,而是「以props帶入React element的話,當component進行re-render,並不會影響透過props帶入之React element指向的記憶體位址,在reconciler階段的檢查步驟時,就會因爲發現到指向相同的記憶體位址,而不重新呼叫這個透過props傳進來的React element」。

當這樣調整完後,也就不會有滾動畫面時,畫面卡卡的狀況出現了。

【前端新手日記】React Component - <Com

結論
最後的最後,也在整理出一些這次的重點內容。
- 狹義的component指的並不是React element本身,也不是畫面長出來的某個區塊,而是能夠創建React element的function,是一個創建React element的藍圖。
- 當建立component之後,透過不同的呼叫方式來使用這個component funciton,會產生不同的結果。如果使用呼叫function的方式,創建出來的React element會是一般html標籤類型的React element,只有透過標籤名稱方式來呼叫,才會創建component類型的React element。
- 如果是一個component類型的React element,在父component創建react element的時候,只是單純標記出這個地方要塞一個component類型的React element,React會在reconciler階段檢查這些建立好的React element之後,才去呼叫這個component類型的React element,這邊也就是所謂的「延遲呼叫」。
- 我們可以利用React畫面更新機制中,透過維持component指向同個記憶體的方式,讓React在reconciler階段在檢查React element的時候,能略過這個component function的re-render。
- props本身並沒有component被re-render時,props自己不會被re-render的特性,只是因為透過props傳入React element時,在component在re-render時,這個透過props傳入的React element能夠依舊維持指向同一個記憶體,也就能在reconciler階段檢查React element的時候,略過呼叫這個記憶體位址相同的component function。


參考資料

React 思維進化:一次打破常見的觀念誤解,躍升專業前端開發者
Elements, children as props, and re-renders

 

arrow
arrow

    文科少女寫程式 發表在 痞客邦 留言(1) 人氣()