理解 React 中的渲染行為
已發表: 2020-11-16與生、死、命運和稅收一起,React 的渲染行為是生活中最偉大的真理和奧秘之一。
讓我們潛入吧!
和其他人一樣,我從 jQuery 開始了我的前端開發之旅。 純基於 JS 的 DOM 操作在當時是一場噩夢,所以這是每個人都在做的事情。 然後慢慢地,基於 JavaScript 的框架變得如此突出,以至於我不能再忽視它們了。
我學的第一個是 Vue。 我度過了一段難以置信的艱難時期,因為組件和狀態以及其他一切都是一個全新的心理模型,將所有東西都融入其中是很痛苦的。但最終,我做到了,並拍拍自己的後背。 恭喜,伙計,我告訴自己,你已經完成了陡峭的攀登; 現在,其餘的框架,如果您需要學習它們,將非常容易。
所以,有一天,當我開始學習 React 時,我意識到我是多麼的大錯特錯。 Facebook 加入 Hooks 並告訴所有人:“嘿,從現在開始使用它,這並沒有讓事情變得更容易。 但是不要重寫類; 上課很好。 其實也不算多,不過還好。 但 Hooks 就是一切,它們就是未來。
知道了? 偉大的!”。
最後,我也翻過了那座山。 但後來我遇到了與 React 本身一樣重要且困難的事情:渲染。

如果您在 React 中遇到過渲染及其奧秘,您就會知道我在說什麼。 如果你沒有,你不知道有什麼適合你!
但是在浪費時間做任何事情之前,最好先問問你會從中得到什麼(不像我,他是一個過度興奮的白痴,會為了它而高興地學習任何東西)。 如果您作為 React 開發人員的生活過得很好,而不必擔心渲染是什麼,那麼為什麼要關心呢? 好問題,所以讓我們先回答這個問題,然後我們會看到渲染實際上是什麼。
為什麼理解 React 中的渲染行為很重要?
我們都通過編寫(這些天,功能性的)組件開始學習 React,這些組件返回稱為 JSX 的東西。 我們還了解到,這個 JSX 以某種方式轉換為頁面上顯示的實際 HTML DOM 元素。 頁面隨著狀態更新而更新,路由按預期更改,一切都很好。 但是這種關於 React 工作原理的觀點是幼稚的,並且是許多問題的根源。
雖然我們經常成功地編寫完整的基於 React 的應用程序,但有時我們會發現應用程序的某些部分(或整個應用程序)非常緩慢。 而最糟糕的部分。 . . 我們不知道為什麼! 我們所做的一切都是正確的,我們沒有看到任何錯誤或警告,我們遵循了組件設計、編碼標準等的所有良好實踐,並且沒有網絡緩慢或昂貴的業務邏輯計算在幕後進行。
有時,這是一個完全不同的問題:性能沒有問題,但應用程序的行為很奇怪。 例如,對身份驗證後端進行 3 次 API 調用,但只對所有其他 API 調用一次。 或者某些頁面被重繪了兩次,同一頁面的兩個渲染之間的可見過渡創建了一個不和諧的用戶體驗。

最糟糕的是,在這種情況下沒有可用的外部幫助。 如果你去你最喜歡的開發論壇問這個問題,他們會回答:“不看你的應用就無法判斷。 你能在這裡附上一個最低限度的工作例子嗎?” 好吧,當然,出於法律原因,您不能附加整個應用程序,而該部分的一個小型工作示例可能不包含該問題,因為它與整個系統的交互方式與實際應用程序中的方式不同。
搞砸了? 是的,如果你問我。
所以,除非你想看到這樣的悲慘日子,否則我建議你培養一種理解——和興趣,我必須堅持; 勉強獲得的理解不會讓你在 React 世界中走得更遠——在這個被稱為 React 渲染的鮮為人知的事情中。 相信我,它並不難理解,雖然它很難掌握,但你會走得很遠,而不必了解每一個角落。
React 中的渲染是什麼意思?
我的朋友,這是一個很好的問題。 我們在學習 React 時不傾向於問它(我知道是因為我沒有),因為“渲染”這個詞可能會讓我們陷入一種錯誤的熟悉感。 雖然字典的含義完全不同(在本次討論中並不重要),但我們程序員已經對它的含義有了一個概念。 當我們閱讀“渲染”這個詞時,使用屏幕、3D API、顯卡和閱讀產品規格會訓練我們的大腦去思考類似於“畫一幅畫”的東西。 在遊戲引擎編程中,有一個渲染器,其唯一的工作就是——準確地說!繪製場景所傳遞的世界。
因此我們認為,當 React “渲染”某些東西時,它會收集所有組件並重新繪製網頁的 DOM。 但是在 React 世界中(是的,甚至在官方文檔中),這並不是渲染的意義所在。 所以,讓我們係好安全帶,真正深入了解一下 React 的內部結構。

您一定聽說過 React 維護所謂的虛擬 DOM,它會定期將其與實際 DOM 進行比較並根據需要應用更改(這就是為什麼您不能將 jQuery 和 React 放在一起——React 需要完全控制DOM)。 現在,這個虛擬 DOM 不像真正的 DOM 那樣由 HTML 元素組成,而是由 React 元素組成。 有什麼不同? 好問題! 為什麼不創建一個小的 React 應用程序並親自看看呢?
為此,我創建了這個非常簡單的 React 應用程序。 整個代碼只是一個包含幾行的文件:
import React from "react"; import "./styles.css"; export default function App() { const element = ( <div className="App"> <h1>Hello, there!</h1> <h2>Let's take a look inside React elements</h2> </div> ); console.log(element); return element; }注意我們在這裡做什麼?
是的,只需記錄 JSX 元素的樣子。 這些 JSX 表達式和組件是我們已經編寫了數百次的東西,但我們很少關注發生了什麼。 如果您打開瀏覽器的開發控制台並運行此應用程序,您將看到一個Object為:

這可能看起來令人生畏,但請注意一些有趣的細節:
- 我們看到的是一個普通的、常規的 JavaScript 對象,而不是一個 DOM 節點。
- 請注意,
props屬性表示它的className為App(這是代碼中設置的 CSS 類),並且該元素有兩個子元素(這也匹配,子元素是<h1>和<h2>標籤) . -
_source屬性告訴我們源代碼從哪裡開始元素的主體。 如您所見,它將文件App.js為源文件並提及第 6 行。如果您再次查看代碼,您會發現第 6 行就在 JSX 開頭標記之後,這是有道理的。 JSX 括號包含React 元素; 它們不是其中的一部分,因為它們稍後會轉換為React.createElement()調用。 -
__proto__屬性告訴我們這個對象派生了它的所有。 來自根 JavaScriptObject的屬性,再次強化了我們在這裡看到的只是日常 JavaScript 對象的想法。
所以,現在,我們了解到所謂的虛擬 DOM 看起來不像真正的 DOM,而是代表當時 UI 的 React (JavaScript) 對象樹。

筋疲力盡的?
相信我,我也是。 在我的腦海裡一遍又一遍地翻動這些想法,試圖以最好的方式呈現它們,然後想出用詞把它們帶出來並重新排列它們——這並不容易。
但是我們分心了!
經歷了這麼多,我們現在可以回答我們所追求的問題:React 中的渲染是什麼?
好吧,渲染是 React 引擎進程遍歷虛擬 DOM 並收集當前狀態、道具、結構、UI 中所需的更改等。React 現在使用一些計算更新虛擬 DOM,並將新結果與實際 DOM 進行比較在頁面上。 這種計算和比較就是 React 團隊官方所說的“和解”,如果你對他們的想法和相關算法感興趣,可以查看官方文檔。
是時候提交了!
渲染部分完成後,React 開始一個稱為“提交”的階段,在此期間它將必要的更改應用於 DOM。 這些更改是同步應用的(一個接一個,儘管很快就會出現一種同時工作的新模式),並且更新 DOM。 React 何時以及如何應用這些更改並不是我們關心的問題,因為它完全在幕後,並且隨著 React 團隊嘗試新事物而不斷變化。
React 應用程序中的渲染和性能
我們現在已經明白,渲染意味著收集信息,它不需要每次都導致視覺 DOM 變化。 我們也知道,我們認為的“渲染”是一個涉及渲染和提交的兩步過程。 我們現在將看到如何在 React 應用程序中觸發渲染(更重要的是重新渲染),以及不了解細節如何導致應用程序性能不佳。
由於父組件的變化而重新渲染
如果 React 中的父組件發生更改(例如,因為其狀態或道具發生更改),React 會沿著該父元素遍歷整個樹並重新渲染所有組件。 如果您的應用程序有許多嵌套組件和大量交互,那麼每次更改父組件時都會在不知不覺中對性能造成巨大影響(假設它只是您想要更改的父組件)。
沒錯,渲染不會導致 React 改變實際的 DOM,因為在協調期間,它會檢測到這些組件沒有任何變化。 但是,它仍然浪費了 CPU 時間和內存,你會驚訝於它加起來的速度有多快。
由於上下文變化而重新渲染
React 的 Context 功能似乎是每個人最喜歡的狀態管理工具(它根本不是為它而構建的)。 這一切都非常方便——只需將最頂層的組件包裝在上下文提供程序中,剩下的就很簡單了! 大多數 React 應用程序都是這樣構建的,但是如果你到目前為止已經閱讀了這篇文章,你可能已經發現了問題所在。 是的,每次更新上下文對象時,都會觸發所有樹組件的大規模重新渲染。

大多數應用程序都沒有性能意識,因此沒有人注意到,但如前所述,在大容量、高交互的應用程序中,這種疏忽可能會造成非常高的成本。
提高 React 渲染性能
那麼,考慮到這一切,我們可以做些什麼來提高應用程序的性能呢? 事實證明,我們可以做一些事情,但請注意,我們只會在功能組件的上下文中討論。 React 團隊非常不鼓勵基於類的組件,並且正在退出。
使用 Redux 或類似的庫進行狀態管理
那些喜歡快速而骯髒的 Context 世界的人往往會討厭 Redux,但它非常受歡迎是有充分理由的。 其中一個原因是性能——Redux 中的connect()函數很神奇,因為它(幾乎總是)正確地只根據需要渲染那些組件。 是的,只要遵循標準的 Redux 架構,性能就免費了。 毫不誇張地說,如果您採用 Redux 架構,您會立即避免大部分性能(和其他)問題。
使用memo()來“凍結”組件
“備忘錄”這個名字來自於記憶化,這是一個用於緩存的花哨名稱。 如果你沒有遇到太多緩存,沒關係; 這是一個淡化的描述:每次您需要一些計算/運算結果時,您都會查看您一直在維護先前結果的地方; 如果你找到了,太好了,只需返回該結果; 如果沒有,請繼續執行該操作/計算。
在直接進入memo()之前,讓我們先看看 React 中不必要的渲染是如何發生的。 我們從一個簡單的場景開始:應用程序 UI 的一小部分,向用戶顯示他們喜歡該服務/產品的次數(如果您在接受用例時遇到困難,想想如何在 Medium 上“鼓掌” ”多次顯示您對文章的支持/喜歡程度)。
還有一個按鈕,可以讓他們將喜歡的次數增加 1。最後,裡面還有另一個組件,可以向用戶顯示他們的基本帳戶詳細信息。 如果您發現這很難理解,請不要擔心; 我現在將為所有內容提供分步代碼(而且內容不多),最後提供一個指向遊樂場的鏈接,您可以在其中弄亂正在運行的應用程序並提高您的理解。
讓我們首先處理有關客戶信息的組件。 讓我們創建一個名為CustomerInfo.js的文件,其中包含以下代碼:
import React from "react"; export const CustomerInfo = () => { console.log("CustomerInfo was rendered! :O"); return ( <React.Fragment> <p>Name: Sam Punia</p> <p>Email: [email protected]</p> <p>Preferred method: Online</p> </React.Fragment> ); };沒什麼花哨的,對吧?
只是一些信息文本(可能已經通過道具傳遞),預計不會隨著用戶與應用程序交互而改變(對於那裡的純粹主義者來說,是的,肯定它可以改變,但重點是,與其他人相比的應用程序,它實際上是靜態的)。 但請注意console.log()語句。 這將是我們知道組件被渲染的線索(記住,“渲染”意味著它的信息被收集和計算/比較,而不是它被繪製到實際的 DOM 上)。
因此,在我們的測試過程中,如果我們在瀏覽器控制台中沒有看到這樣的消息,那麼我們的組件根本沒有被渲染; 如果我們看到它出現了 10 次,這意味著該組件被渲染了 10 次; 等等。
現在讓我們看看我們的主要組件如何使用這個客戶信息組件:
import React, { useState } from "react"; import "./styles.css"; import { CustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <CustomerInfo /> </div> </div> ); } 因此,我們看到App組件通過useState()鉤子進行了內部狀態管理。 此狀態不斷計算用戶喜歡該服務/站點的次數,並且最初設置為零。 就 React 應用程序而言,沒有什麼挑戰性的,對吧? 在 UI 方面,情況如下所示:

這個按鈕看起來太誘人了,至少對我來說是這樣! 但在此之前,我將打開瀏覽器的開發控制台並清除它。 之後,我將按下按鈕幾次,這就是我所看到的:

我已經點擊了 19 次按鈕,正如預期的那樣,總點贊數為 19。配色方案很難閱讀,所以我添加了一個紅色框來突出顯示主要內容: <CustomerInfo />組件被渲染了20次!
為什麼是20?
一次是在最初渲染所有內容時,然後是點擊按鈕時的 19 次。 按鈕改變totalLikes ,這是<App />組件內部的一個狀態,結果,主組件重新渲染。 正如我們在本文前面部分所了解的,其中的所有組件也會重新渲染。 這是不需要的,因為<CustomerInfo />組件在過程中沒有改變,但對渲染過程有貢獻。
我們如何防止這種情況發生?
正如本節的標題所說,使用memo()函數創建<CustomerInfo />組件的“保留”或緩存副本。 使用 memoized 組件,React 查看它的 props 並將它們與之前的 props 進行比較,如果沒有變化,React 不會從該組件中提取新的“渲染”輸出。
讓我們將這行代碼添加到我們的CustomerInfo.js文件中:
export const MemoizedCustomerInfo = React.memo(CustomerInfo);是的,這就是我們需要做的! 現在是時候在我們的主要組件中使用它,看看是否有什麼變化:
import React, { useState } from "react"; import "./styles.css"; import { MemoizedCustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <MemoizedCustomerInfo /> </div> </div> ); }是的,只改變了兩行,但我還是想展示整個組件。 UI 方面沒有任何改變,所以如果我試一試新版本並按下“贊”按鈕幾次,我會得到:

那麼,我們有多少控制台消息?
只有一個! 這意味著除了初始渲染之外,根本沒有觸及組件。 想像一下真正大規模應用程序的性能提升! 好的好的,我承諾的代碼遊樂場的鏈接在這裡。 要復制前面的示例,您需要從CustomerInfo.js導入並使用CustomerInfo而不是MemoizedCustomerInfo 。
也就是說, memo()並不是你可以隨處灑落並期待神奇結果的神奇沙子。 過度使用memo()也會在你的應用程序中引入棘手的錯誤,有時,只會導致一些預期的更新失敗。 關於“過早”優化的一般建議也適用於此。 首先,按照您的直覺構建您的應用程序; 然後,做一些深入的分析,看看哪些部分很慢,如果記憶化的組件看起來是正確的解決方案,然後才引入這個。
“智能”組件設計
我把“智能”放在引號中是因為:1)智能是高度主觀和情境化的; 2) 所謂的聰明行為往往會產生不愉快的後果。 所以,我對本節的建議是:不要對你正在做的事情過於自信。
除此之外,提高渲染性能的一種可能性是設計和放置組件的方式略有不同。 例如,可以重構子組件並將其移動到層次結構的上方某處,以避免重新渲染。 沒有規則說“ChatPhotoView 組件必須始終位於 Chat 組件內”。 在特殊情況下(這些是我們有數據支持的證據表明性能受到影響的情況),彎曲/打破規則實際上是一個好主意。
結論
總體而言,可以做更多的事情來優化 React 應用程序,但是由於本文是關於渲染的,所以我限制了討論的範圍。 無論如何,我希望你現在能更好地了解 React 底層發生了什麼,渲染實際上是什麼,以及它如何影響應用程序性能。
接下來,我們來了解一下什麼是 React Hooks?
