# React - Behide the Scenes ###### tags: `Javascript, React` # 這個 module 在說什麼? * How does React work behide the scenes? * Understanding the virtual DOM& real DOM updates * Understanding State & State updates  React 只會在乎: 1. Props 2. Context 3. State 4. 當然還有更多 API 不過上述是核心 所以當 React 想要修改真實 DOM 上面的資料時必須透過 ReactDom 作為中介傳遞 virtualDOM 的更動來更新至 DOM 上 # virtual DOM 這時候最大的問題是 components 這邊如何跟 real Dom 作用 這邊會在 React 處理 component tree(也就是 App.js 底下的樹狀結構) 目前應該要長什麼樣子以及他將會變成什麼樣子(這邊就是 virtual DOM) 然後 ReactDom 會接收到不一樣的地方並且操作 real DOM 把修改更新上去頁面  # re-excuted component func and re-evaluate page is not the samething 這邊要非常注意  1. 當 components 的 props, states, contexts 改變時 re-evaluate 會發生 2. 這時 React 會 re-excute component 3. 然後 ReacrDom 會在此時修改 realDOM 去 re-render 那些不同的地方,而非全部更新因此可以節省很多的效能  因為 realDOM 的修改是非常耗能的行為,所以透過這樣對比 virtualDOM 以及 realDOM 不同的地方,不 re-render 整個頁面是 React 運作的方式 ## 範例 假設目前的 virtual DOM 要做出改變,要加個 p tag 進去,這時候這個 p tag 就是 virtual DOM 跟 real DOM 的改變,所以 p tag 會被插進去 real DOM 也就只修改這個地方  ### 程式碼解釋  會得到一個畫面可以 toggle p tag 的內容,當點擊按鈕時會讓 p tag 出現或是消失藉由 useState 改變狀態來幫助其內容的呈現   這邊要來證明 state, context, props 的改變會讓整個 component re-excuted re-evaluated 只需要在 component 中印出文字即可知道當上述內容改變時,是否會讓整個函式重新跑一次  在一開始畫面 load 進去的時候會立馬印出一次,因為那是 React 第一次跑這個 component 並把內容帶入瀏覽器 當按下按鈕,畫面更新出 p tag 時,就印出第二遍了,這也證明了所有的 state 改變都會引發 component 的重新執行,因為印出了第二遍  可以從開發者工具觀察到,如果有畫面的更新元素會閃一下,這邊只有 p tag 新增進來的時候有閃以及 p tag 被移除了時候父層的 div 有閃,表示跟 virtual DOM 做完對比 real DOM 只有這些地方有不同,所以只有這些地方有做更動  ### App.js 重新執行內部的 components 也會跟著一起重新執行 這邊把 p tag 拉出去變成 DemoOutput component 這邊用來解釋儘管傳入 DemoOutput 的值都是 false 畫面因此不會更新,但是因為 DemoOutput 還是 App.js 的 child component,所以當 state 改變(點擊按鈕時)在 App.js 內部時,造成 App.js re-excuted/ re-evaluated 時,其內部的 components 也會跟著重新被跑一次  儘管畫面沒有更新 DemoOutput 裡面依舊有印出那個值來,就是最好的證明  Button component 也一樣是 App.js 的 child component 所以也會印出其值  由此可證畫面的 re-rendering 跟 component 的 re-excuted/ re-evaluated 不一定有關  ### 這帶出一個大問題就是 function component state 造成的 re-excuted/ re-evaluated 就讓其 child component tree 底下所有的 components 重新跑,會造成效能浪費(因為有些是非必要的) # 避免不必要的 re-evaluations 使用 React.memo() 這邊介紹使用在 function components 中的用法使用 React.memo() 來達成( class component 會在後面介紹  這邊的 memo 運作的方式會是他會他會監聽其參數的 component 的 props 是否有改變 所以如果 parent component state 有所改變,但是傳進來參數 component 的 props 沒有改變的話, component 的 re-excuted/ re-evaluate 就會被忽略 下圖可以看到 DemoOutput 的 log 內容被忽略了,並且可以知道的是其底下的 child components 也同樣不會被 re-excuted/ re-evaluated  ## 使用 memo 的代價 要做到監聽其參數的 component 的 props 是否有改變,也是需要耗能的,React 必須儲存先前的 props 然後對比當下的 props 看是否有改變,因此必須判斷你要使用 memo 的 comoponent 是否值得花這樣的效能去做處理 不過 memo 依舊是一個很棒的工具,當 component tree 非常龐大並且 props 非常多時,可以使用 memo 避免一些不必要的 re-render ,精準的使用在一些關鍵的 components 上而不是所有 components 都使用,會讓程式更有效率 ## memo 參數的 props 傳值或是傳址的區別 假設我們在 DemoOutput, Button 都使用 memo 因為其實都是寫死的內容都適合使用  但是問題來了,經過操作之後發現 DemoOutput 會順利的被阻斷,但是 Button 卻沒有,原因就是傳值跟傳址的區別 DemoOutput 傳的是純值,因此儘管每一次 parent re-excuted 後得出來的 props 會得到新的 false 但是因為他是純值所以 `false === false` 是成立的 但是 Button 傳入的 props 是函式,函式就是物件,物件是傳址的 `toggleParagraphHandler === toggleParagraphHandler` 是不成立的,因為他們在記憶體中的位置不同 memo 就這樣被物件擊敗了嗎?並沒有下個篇章中會有解答 # 避免不必要的函式 re-creation 使用 useCallback() 這邊就要解釋如何使用物件當作 React.memo() 的 props ,透過使用 useCallback() 來操作產生的物件 `useCallback()` 的作用在於,它會儲存一個 function 在 components 執行之間,告訴 React 被儲存的這個 function 不會被重新創造在每一次 component 執行之間,透過這樣的方式 function 物件,就不會被重新創造因而可以操作在 `React.memo()` 之中摟 ## `useCallback()` 做的事情跟下面例子很像: 它會儲存我們所選擇的函式在 React 內部的儲存空間中,因此我們就可以重複使用我們存的那個函式物件當其所在 component function 執行時 ```javascript= let obj1 = {}; let obj2 = {}; obj1 === obj2; // false obj1 = obj2; obj1 === obj2 // true ``` ## 使用方法如下 * 我們要操作在 toggleParagraphHandler 這個函式中 * 直接使用一個箭頭函式包裹住其內容 * 放入 array 當作 dependency 來操作用法跟 useEffect 一樣作為監聽使用放入的 state ,這邊可以放 setShowParagraph 不過 React 已經保證過 useState 返回的操作 state 函式內容不會被變更所以可以保持為空,如果有放入內容則會監聽其內容,如果改變了則會更新記憶的函式到最新  這個時候我們再去操作 memo 內放入 toggleParagraphHandler 的 Button 發現 memo 的功能生效了,也就代表函式已經被 React 儲存好並且不會隨著 component function 作出更新函式的動作了 # 介紹更多 useCallback() 以及他的 dependencies 範例處會新增一個 allow toggle 按鈕來開關 toggle paragraph 按鈕 這邊要介紹到 dependencies 的部分就要先了解 JS 的 closure 特性,也就是為了取用到外部的變數,函式會儲存要使用的外部變數鎖在函式內部做使用,但是搭配 useCallback() 的特性就會造成取用到的變數內容是過期的,因此使用了 dependencies 就可以監聽放入的內容( `[]`內的內容),當 `[]` 內容改變時,React 會更新被記憶的 function 如此一來就可以取得最新的 closure 儲存的變數摟! 這邊我們新增一個按鈕來讓開關 toggleParagraphHandler 並且多使用一個 state allowToggle 來做到這件事情   必須 allow 按下去之後才會可能 toggle paragraph  目前因為 dependencies 欄位是空的,所以 useCallback() 是在任何情況下都不會更新被記憶的函式,並且 closure 內容都是沒有更新的 allowToggle 的 state 也就是初始值 false ,所以目前怎麼點擊都是不會有效果的  這邊在 dependencies 內放入 allowToggle 後代表, useCalback() 會監聽它,當 allowToggle 更新時,就會重新記憶內部的 function ,這時候 function closure 內部的 allowToggle 也就可以被更新了!按鈕就可以正常使用了 # In Summary * 在 React 中你會運作的幾乎都是 Functional components ,他們的工作最終都是要輸出 JSX 到畫面中 * 在 React component 中,可能會使用到 state, props, context ,不過其他最終他們都是 state 的改變,用來所在的改變 component 或是相關的 data 甚至是整個 app * 每當你在 component 中改變了 state ,此時就會引發 re-evaluated 意思是當下的 component 會 re-executed * React 會使用下最新的 evaluation 結果去對比之前的,並把不一樣的地方給 React DOM 使用,這時候 React DOM 就會應用到 real DOM 上面更新瀏覽器畫面,並且只會新增新的部分,不會動到其他地方 * 當 re-evaluated, re-executed 發生時, component 中所有底下使用的 component 中的 function 也會同時重新被執行一遍,所以為了避免一些不必要的 child component re-execution 就可以操作 memo 來達成,memo 會監聽其 props 如果沒有改變則不會重新執行裡面的 functions * 這時候就會注意到一些 memo 的小漏洞,像是 re-execution 時,如果函式內操作了函式這時候因為物件是傳址的緣故,儘管 function 是一樣的但是在 stack 中的指向是不一樣的地方,因此會被判定為 props 有改變,所以 memo 會達不到所要的效果 * 這時候就是 `useCallback()` 的使用時機,讓 React 記憶函式後不會在每次 component re-execution 時重新創造函式內容,並且使用 dependencies 來確保當 props 修改時,才會更新函式,既可以操作 memo 又可以保證其 props 不會過期 # Components & States > 只要跟 re-rendering components 有關,所有的東西都會回歸到 state 在上一篇的最後有提出一個疑問,如果 component re-execution 發生了,那 useState 也會重新被執行,照理說其中的 state 也會更新,但是卻沒有 原因是 React 會確保 state 被記憶起來,當 component 第一次 render 到畫面上,並會使用 useState 的 default 值來呈現,接續下來的 re-execution 都不會再有新的 state 被更新出來,React 會認出屬於這個 component 的 state 並且只會在有需要的時候才會更新這個 state,只有一個例外就是當 component 被移除又在被重新 render 時才會初始化 state ,這個觀念也適用於 useReducer ## State updates & Scheduling  1. 假設有個 setProduct 按鈕可以切換商品內容,初始值為 DVD 2. 使用者點擊了按鈕並且輸入 Book 來修改 state 3. React 對這個 update state 行為做預約排序,如果有更急的可以先處理,比方說 input 的值輸入 4. 這時候如果使用者又點了一次想改成 carpet 則 state 修改順序一定會按照 DVD => Book => Carpet 這個順序走 5. React 會 re-evaluated component state 後 re-executed component  正因為 複數個 state update 可以同時被排序,因此有可能 state 在輪到他執行之前,就又被改變了有可能會有 state 過期發生,因此需要使用函式的方式來使用 preState 抓取最新的 state 避免過期(useEffect 有 dependencies 也可以避免 state 過期)  ## state batch 批次處理 state ,剛剛提到 React 會針對 state 會做預約排序不會馬上處理他們,那如果同一個 function 中處理兩種 state 他們會分開成兩條路線做預約排程嗎?  答案是不會,他們會被同捆包成同一包 state update ,只要他們是寫在同一個 function 中並且沒有非同步的寫法,他們就會同步進行 # Optimizing with useMemo() 這邊需要操作另一個扣的範例來說明: ## App.js  輸出的範例內容如下 * 點擊按鈕後 title 內容會跟新 * 可以發現 從 App 傳下的 props 是沒有經過排序的 items 內容  ## DemoList.js 在此檔案中使用 sort 來回傳排序後的 items array ,這邊要解釋的是假設要 sort 的資料量非常的大則會非常耗能,所以你不會想要每次 re-evaluated component 的時候,他都跑一次  這邊我們從之前章節學到的,可以使用 memo 來處理,如果 items 沒有改變的話則不會更新 DemoList.js 的內容  但是 DemoList 取得的 props 是 array 代表他是傳址的,所以當我們點擊 Change List title 按鈕時,component re-execution 這時候傳進來的 props 也指向了不同的位置因此代表 props 也更新了,因此 memo 這時候就沒辦法執行它在這邊應該執行的功能 就算不是點擊了按鈕,也可能因為 app.js 本身的任何改動都會導致其 child component DemoList 的 props 因而有所變動而導致 sort 再次觸發 有鑒於不想要每次都跑到那些很好效能的 component ,在這邊的範例中 sorting 就是耗能的任務 ## 解法使用 useMemo() 來救場啦 使用 useMemo 包裹著 sort ,讓他只有在 dependencies 有修改的時好也就是 items 有更動時才會跑 sort  然後我們興高采烈地按下 Change List Title 按鈕以為會成功時,發現失敗了!為什麼他又跑了一次 sort 呢? 很簡單因為 items 確實在按下按鈕的時候被更新的,因為他是 array ,所以也要把它包起來就萬事OK!! 這邊 dependencies 就為空就好因為他的 items 是寫死的 
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up