React從基礎入門到高級實戰:React 基礎入門 - React 的工作原理:虛擬 DOM 與 Diff 算法

React 的工作原理:虛擬 DOM 與 Diff 算法

引言

React 是現代前端開發的明星框架,它的出現徹底改變了我們構建用戶界面的方式。無論是動態的 Web 應用還是復雜的單頁應用(SPA),React 都能以高效的渲染機制和簡潔的組件化開發模式讓開發者受益匪淺。那么,React 為什么如此強大?答案就在于它的兩大核心機制:虛擬 DOM(Virtual DOM)Diff 算法

簡單來說,虛擬 DOM 是 React 的“秘密武器”,它在內存中模擬真實 DOM 的結構,讓 React 能夠快速計算界面變化的部分,并以最小的代價更新頁面。而 Diff 算法則是 React 的“智能大腦”,它通過高效比較新舊虛擬 DOM 樹,找出需要更新的地方,避免了傳統 DOM 操作的低效和繁瑣。

這篇文章的目標是帶你從零開始,深入探索虛擬 DOM 和 Diff 算法的奧秘。我們將從基礎概念講起,逐步剖析它們的實現細節,并通過豐富的代碼示例和實踐案例,讓你不僅理解原理,還能將知識應用到實際開發中。無論你是 React 新手,還是希望深入掌握其內部機制的開發者,這篇文章都將為你提供全面而清晰的指導。


1. 虛擬 DOM 簡介

1.1 什么是虛擬 DOM?

虛擬 DOM 是 React 為了提升渲染性能而設計的一種技術。它本質上是一個 JavaScript 對象,用來描述真實 DOM 的結構和內容。與直接操作真實 DOM 不同,React 先在內存中創建一個虛擬 DOM 樹,通過比較新舊虛擬 DOM 的差異,再將變化應用到真實 DOM 上。

通俗比喻
想象你在寫一篇文章。如果每次修改都直接在紙上涂改,你會浪費很多時間擦掉舊內容、寫上新內容,甚至可能弄亂整頁紙。更好的方法是先在電腦上編輯草稿,改動滿意后再打印到紙上。虛擬 DOM 就像這個“草稿”,它讓 React 在內存中快速調整界面結構,最后一次性更新真實 DOM,省時又高效。

1.2 為什么需要虛擬 DOM?

在傳統的 Web 開發中,開發者需要通過 JavaScript 直接操作 DOM,比如添加、刪除或修改元素。然而,真實 DOM 操作非常昂貴,因為每次改動都可能觸發瀏覽器的重排(reflow)和重繪(repaint),這些過程會消耗大量計算資源。

虛擬 DOM 的出現解決了這個問題。它的主要優勢包括:

  • 性能提升:虛擬 DOM 允許 React 在內存中批量處理更新,只將必要的改動應用到真實 DOM,減少重排和重繪的次數。
  • 跨平臺能力:虛擬 DOM 不僅可以渲染到瀏覽器的真實 DOM,還可以通過 React Native 等技術渲染到移動端或其他平臺。
  • 開發體驗優化:開發者無需手動管理復雜的 DOM 操作,只需關注組件的狀態和屬性(Props),React 會自動完成渲染工作。

1.3 虛擬 DOM 的結構

虛擬 DOM 是一個樹形結構,每個節點是一個 JavaScript 對象,包含以下核心屬性:

  • type:節點的類型,可以是 HTML 標簽(如 'div''span')或自定義 React 組件。
  • props:節點的屬性,比如 classNamestyle 或事件監聽器。
  • children:子節點,可以是單個節點、節點數組或純文本。

示例

{type: 'div',props: { className: 'container', id: 'main' },children: [{ type: 'h1', props: { children: 'Welcome to React' } },{ type: 'p', props: { children: 'Learn about Virtual DOM' } }]
}

這個虛擬 DOM 表示一個 <div> 元素,包含一個 <h1> 和一個 <p> 子元素。React 正是通過這樣的對象結構來描述界面的。


2. 虛擬 DOM 的工作流程

2.1 虛擬 DOM 的創建

在 React 中,虛擬 DOM 的創建始于 JSX。JSX 是一種類似 HTML 的語法糖,開發者用它來描述組件的結構,最終會被編譯為 React.createElement 函數調用,生成虛擬 DOM 對象。

示例

function App() {return (<div className="app"><h1>Hello, React!</h1></div>);
}

編譯后:

function App() {return React.createElement('div',{ className: 'app' },React.createElement('h1', null, 'Hello, React!'));
}

React.createElement 返回的就是一個虛擬 DOM 對象,描述了組件的層級結構。

2.2 虛擬 DOM 的更新

當組件的狀態(state)或屬性(props)發生變化時,React 會重新調用組件的渲染函數,生成一個新的虛擬 DOM 樹。然后,React 會將新樹與舊樹進行比較,找出差異,再將這些差異應用到真實 DOM 上。

更新流程

  1. 狀態或屬性變化:比如用戶點擊按鈕,觸發 setState
  2. 生成新虛擬 DOM:React 重新渲染組件,生成新的虛擬 DOM 樹。
  3. 差異比較:通過 Diff 算法,React 計算新舊虛擬 DOM 的差異。
  4. 更新真實 DOM:將差異批量應用到真實 DOM,完成界面更新。

這個過程的核心在于“比較”和“更新”的高效性,而這正是 Diff 算法的舞臺。


3. Diff 算法詳解

Diff 算法是 React 的核心優化機制,它負責比較新舊虛擬 DOM 樹,找出最小的更新操作。React 的 Diff 算法基于以下三個假設和策略:

  1. 分層比較(Tree Diff):只比較同一層級的節點,跨層級操作較少。
  2. 組件類型比較(Component Diff):相同類型的組件可以復用,不同類型則銷毀重建。
  3. 同層元素比較(Element Diff):通過 key 屬性優化同層節點的匹配效率。

圖解 Diff 算法

舊虛擬 DOM       新虛擬 DOMA                A/ \              / \
B   C            B   D|                |E                F
  • A 節點相同,復用。
  • C 節點類型不同,替換為 D。
  • E 節點替換為 F。

下面我們逐一深入探討這三個階段。

3.1 Tree Diff:分層比較

React 的 Diff 算法首先從樹的根節點開始,逐層比較新舊虛擬 DOM。如果某層的節點類型不同,React 會直接刪除舊節點及其所有子節點,然后用新節點替換。這種策略基于一個假設:不同類型的節點通常會生成完全不同的 DOM 結構,繼續比較子節點沒有意義。

示例

// 舊虛擬 DOM
{type: 'div',props: { className: 'box' },children: [{ type: 'span', props: { children: 'Text' } }]
}// 新虛擬 DOM
{type: 'section',props: { className: 'box' },children: [{ type: 'p', props: { children: 'Text' } }]
}
  • 根節點類型從 'div' 變為 'section'
  • React 刪除舊的 <div> 及其 <span> 子節點,創建新的 <section><p>

優點:分層比較避免了對整棵樹的逐一遍歷,極大提高了效率。

3.2 Component Diff:組件類型比較

在比較同一層級的節點時,如果節點是一個 React 組件,React 會先檢查組件的類型:

  • 相同類型組件:React 繼續比較其 propsstate,更新內部狀態并復用實例。
  • 不同類型組件:React 銷毀舊組件實例(包括其子樹),創建新組件實例。

示例

// 舊渲染
function OldComponent() {return <div>Old</div>;
}
<OldComponent />// 新渲染
function NewComponent() {return <div>New</div>;
}
<NewComponent />
  • 組件類型不同,React 銷毀 OldComponent,創建 NewComponent

注意:即使兩個組件渲染的 DOM 結構相同,類型不同也會觸發重建,因此盡量保持組件類型的穩定性。

3.3 Element Diff:同層元素比較

對于同一層級的普通元素(如 <li><div>),React 會逐一比較它們的類型和屬性。如果是列表渲染,React 還會利用 key 屬性來優化比較效率。

Element Diff 的三種操作

  • 插入:新樹中有舊樹中沒有的節點。
  • 刪除:舊樹中有新樹中沒有的節點。
  • 移動:節點在新舊樹中都存在,但位置不同。

key 的作用
key 是 React 識別節點的唯一標識,幫助 Diff 算法快速匹配新舊節點。沒有 key 時,React 按順序比較,效率低下;有了 key,React 能準確判斷節點的移動、插入和刪除。

示例

// 舊列表
<ul><li key="a">A</li><li key="b">B</li><li key="c">C</li>
</ul>// 新列表
<ul><li key="a">A</li><li key="d">D</li><li key="b">B</li>
</ul>
  • key="a" 的節點不變。
  • key="c" 的節點被刪除。
  • key="d" 的節點被插入。
  • key="b" 的節點移動到新位置。

性能對比

  • key:React 按順序逐一比較,可能導致所有節點重建。
  • key:React 只更新必要的部分,減少 DOM 操作。

4. 虛擬 DOM 與真實 DOM 的關系

虛擬 DOM 是 React 的“中間人”,它在內存中模擬真實 DOM 的結構和變化。React 的渲染流程可以分為以下幾步:

  1. 初次渲染:根據初始虛擬 DOM 樹創建真實 DOM。
  2. 狀態變化:生成新的虛擬 DOM 樹。
  3. Diff 比較:計算新舊虛擬 DOM 的差異。
  4. 批量更新:將差異應用到真實 DOM。

性能優勢

  • 內存操作比真實 DOM 操作快得多。
  • 批量更新減少了瀏覽器重排和重繪的頻率。

圖解(文字描述):
想象兩棵樹:舊虛擬 DOM 和新虛擬 DOM。React 用 Diff 算法“剪枝”,只保留需要更新的部分,然后將這些“剪枝”結果同步到真實 DOM 上。


5. 實踐案例:計數器應用

讓我們通過一個簡單的計數器應用,觀察虛擬 DOM 和 Diff 算法的實際工作過程。

以下是完整的代碼:

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>React 計數器</title><script src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.development.js"></script><script src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.development.js"></script><script src="https://cdn.tailwindcss.com"></script>
</head>
<body><div id="root" class="p-8 bg-gray-100 min-h-screen flex items-center justify-center"></div><script type="text/babel">function Counter() {const [count, setCount] = React.useState(0);console.log('渲染 Counter 組件,新 count 值:', count);return (<div className="bg-white p-6 rounded-lg shadow-lg text-center"><h1 className="text-3xl font-bold mb-4">計數器</h1><p className="text-xl mb-6">當前計數: <span className="font-semibold">{count}</span></p><buttononClick={() => setCount(count + 1)}className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition">增加</button></div>);}const root = ReactDOM.createRoot(document.getElementById('root'));root.render(<Counter />);</script>
</body>
</html>

運行步驟

  1. 將代碼保存為 index.html 文件。
  2. 用瀏覽器打開,點擊“增加”按鈕。
  3. 打開開發者工具(F12),在控制臺觀察每次渲染的日志。

觀察虛擬 DOM 更新

  • 初次渲染:React 創建初始虛擬 DOM,渲染為真實 DOM,顯示 count: 0
  • 點擊按鈕setCount 觸發狀態更新,React 生成新虛擬 DOM。
  • Diff 過程:React 比較新舊虛擬 DOM,發現只有 <span> 的文本從 0 變為 1
  • 真實 DOM 更新:React 只更新 <span> 的內容,其他部分保持不變。

通過這個案例,你可以看到虛擬 DOM 和 Diff 算法如何高效地處理局部更新,避免了整棵 DOM 樹的重建。


6. 性能優化技巧

虛擬 DOM 和 Diff 算法為 React 提供了良好的性能基礎,但開發者仍需掌握一些優化技巧,以進一步提升應用效率。

6.1 正確使用 key

在列表渲染中,始終為每個元素提供穩定且唯一的 key。避免使用數組索引作為 key,因為索引會隨列表變化而改變,可能導致 Diff 算法誤判。

錯誤示例

items.map((item, index) => <li key={index}>{item}</li>)

正確示例

items.map((item) => <li key={item.id}>{item.name}</li>)

6.2 避免不必要渲染

使用 React.memoshouldComponentUpdate 防止組件在 props 或 state 未改變時重渲染。

示例

const Child = React.memo(function Child({ value }) {console.log('Child 渲染');return <div>{value}</div>;
});

6.3 按需加載組件

使用 React.lazySuspense 實現組件的動態加載,減少初始加載時間。

示例

const LazyComponent = React.lazy(() => import('./LazyComponent'));function App() {return (<Suspense fallback={<div>加載中...</div>}><LazyComponent /></Suspense>);
}

7. 進階內容:React 19 新特性

React 19 引入了一些令人興奮的新特性,進一步優化了虛擬 DOM 和 Diff 算法的性能。

7.1 并發渲染

并發渲染(Concurrent Rendering)允許 React 在渲染過程中暫停和恢復任務,提高應用的響應性。比如,當用戶輸入時,React 可以優先處理輸入事件,再繼續渲染其他部分。

7.2 Server Components

Server Components 將部分組件邏輯移到服務器執行,客戶端只接收渲染結果。這減少了客戶端的計算負擔,同時保留了虛擬 DOM 的高效更新能力。

影響

  • 虛擬 DOM 的生成和 Diff 過程可能部分發生在服務器端。
  • 客戶端只需處理少量動態更新,進一步提升性能。

8. 總結與展望

虛擬 DOM 和 Diff 算法是 React 高效渲染的基石。通過在內存中模擬真實 DOM,React 能夠快速比較和計算界面變化;借助 Diff 算法的分層比較和 key 優化,React 確保了更新的高效性和準確性。

掌握這些原理不僅能讓你更好地理解 React,還能幫助你在開發中應用性能優化技巧,比如合理使用 key、避免不必要渲染等。隨著 React 19 的到來,虛擬 DOM 和 Diff 算法的潛力將被進一步挖掘,值得每位開發者持續關注。

希望這篇文章能讓你對 React 的核心機制有全面而深入的理解!如果有任何疑問,歡迎隨時交流。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/84341.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/84341.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/84341.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

解釋一下NGINX的反向代理和正向代理的區別?

大家好&#xff0c;我是鋒哥。今天分享關于【解釋一下NGINX的反向代理和正向代理的區別?】面試題。希望對大家有幫助&#xff1b; 解釋一下NGINX的反向代理和正向代理的區別? NGINX的反向代理和正向代理的區別主要體現在它們的功能和使用場景上。下面我會詳細解釋它們的定義…

Python學習——執行python時,鍵盤按下ctrl+c,退出程序

在 Python 中&#xff0c;當用戶按下 CtrlC 時&#xff0c;程序默認會觸發 KeyboardInterrupt 異常并終止。 1. 捕獲 KeyboardInterrupt 異常&#xff08;推薦&#xff09; 使用 try-except 塊直接捕獲 KeyboardInterrupt 異常&#xff0c;適用于簡單場景。 示例代碼&#xff…

C++ 反向迭代器(Reverse Iterator)實現詳解

目錄 1. 反向迭代器概述 2. 代碼實現分析 3. 關鍵點解析 3.1 模板參數設計 3.2 核心操作實現 4. 使用示例 1. 反向迭代器概述 反向迭代器是STL中一種重要的適配器&#xff0c;它允許我們以相反的順序遍歷容器。本文將詳細講解如何實現一個自定義的反向迭代器模板類。 2.…

動態DNS管理:【etcd+CoreDNS】 vs【BIND9】便捷性對比

對比 BIND9 集群和 etcdCoreDNS 集群在便捷性方面&#xff0c;通常情況下&#xff0c;對于需要動態、頻繁變更 DNS 記錄以及追求云原生和自動化集成的場景&#xff0c;etcdCoreDNS 方案更加便捷。 然而&#xff0c;“便捷性”也取決于具體的應用場景、團隊的技術棧和運維習慣。…

基于大模型的短暫性腦缺血發作預測與干預全流程系統技術方案大綱

目錄 一、系統概述二、系統架構(一)數據采集層(二)大模型核心層(三)應用服務層(四)數據存儲與管理層三、全流程技術方案(一)術前階段(二)術中階段(三)術后階段(四)并發癥風險預測(五)手術方案制定(六)麻醉方案制定(七)術后護理(八)統計分析(九)技術驗…

MSP430通用電機控制代碼(Motor)設計與實現

一、代碼結構概覽 // Motor.h // Motor.h #ifndef __MOTOR_H_ #define __MOTOR_H_#include "A_include.h"void Motor_Init(void); // 初始化函數 void PWM_SET(int duty0, int duty1); // PWM設置函數#endif// Motor.c // Motor.c #include "Motor.h"…

25年軟考架構師真題(回憶更新中)

論文題: 系統負載均衡設計方法事件驅動架構多模型數據庫應用軟件測試架構案例分析: 必選題:1.1填寫質量屬性的質量屬性名 1.2解釋器風格架構的組成圖填空,以及解釋為什么該模型適用解釋器風格 選做題1redis2.1全量復制的流程圖 <

優化用戶體驗:攔截瀏覽器前進后退、刷新、關閉、路由跳轉等用戶行為并彈窗提示

&#x1f9d1;?&#x1f4bb; 寫在開頭 點贊 收藏 學會&#x1f923;&#x1f923;&#x1f923; 需求 首先列舉一下需要攔截的行為&#xff0c;接下來我們逐個實現。 瀏覽器前進后退標簽頁刷新和關閉路由跳轉 1、攔截瀏覽器前進后退 這里的實現是核心&#xff0c;涉及到大…

Docker:容器化技術

引言 傳統部署環境逐漸不適應現在的企業開發&#xff0c;為了追求更加輕量&#xff0c;更加容易管理項目&#xff0c;引入了docker容器化技術去實現更加高效的部署環境。 一.docker風光下的內核功能和常用命令 1.docker容器和虛擬機的區別 我們在底層和應用層之間引入了一層do…

ping命令常用參數以及traceout命令

在網絡故障排查和性能分析中&#xff0c;ping和 traceroute&#xff08;Windows中通常稱為 tracert&#xff09;是兩個極為重要的工具。它們幫助診斷網絡連接問題&#xff0c;了解數據在網絡中的傳輸路徑。下面將詳細介紹這兩個命令的常用參數及其應用。 ping命令 ping命令用…

SpringBoot開發——Spring Boot異常處理全攻略:五大方案實戰對比

文章目錄 一、血淚教訓:異常處理的代價二、五大異常處理方案詳解2.1 全局異常處理(推薦方案)2.2 控制器級處理2.3 HTTP狀態碼注解2.4 ResponseEntity精細控制2.5 自定義異常體系(企業級方案)三、五大方案對比決策表四、四大避坑指南4.1 異常吞噬陷阱4.2 循環依賴問題4.3 異…

CodeBuddy 實現圖片轉素描手繪工具

本文所使用的 CodeBuddy 免費下載鏈接&#xff1a;騰訊云代碼助手 CodeBuddy - AI 時代的智能編程伙伴 前言 最近在社交媒體上&#xff0c;各種素描風格的圖片火得一塌糊涂&#xff0c;身邊不少朋友都在分享自己的 “素描照”&#xff0c;看著那些黑白線條勾勒出的獨特韻味&a…

2025.05.21華為暑期實習機考真題解析第二題

?? 點擊直達筆試專欄 ??《大廠筆試突圍》 ?? 春秋招筆試突圍在線OJ ?? 筆試突圍OJ 02. 災區物資調度路徑規劃 問題描述 在一次嚴重的自然災害后,LYA負責協調救援物資的配送工作。救援區域包含多個受災鄉鎮和一個物資集結點,各個地點之間的道路狀況各異,有些甚至…

Gartner《Optimize GenAI Strategy for 4 Key ConsumerMindsets》學習心得

一、引言 在當今數字化營銷浪潮中,生成式人工智能(GenAI)正以前所未有的速度重塑著市場格局。GenAI 既是一場充滿機遇的變革,也是一場潛在風險的挑戰。一方面,絕大多數 B2C 營銷領導者對 GenAI 賦能營銷抱有極高期待,他們看到了 GenAI 在提升時間與成本效率方面的巨大潛…

探索鏈表的奇妙世界:從基礎到高級應用

鏈表是計算機科學中一種基礎且重要的數據結構&#xff0c;它如同一條由珠子串成的項鏈&#xff0c;每個珠子&#xff08;節點&#xff09;都包含著數據和指向下一個珠子的線索。 與數組相比&#xff0c;鏈表在插入和刪除操作上更加靈活&#xff0c;無需預先分配固定大小的內存…

黑馬點評雙攔截器和Threadlocal實現原理

文章目錄 雙攔截器ThreadLocal實現原理 雙攔截器 實現登錄狀態刷新的原因&#xff1a; ? 防止用戶會話過期&#xff1a;通過動態刷新Token有效期&#xff0c;確保活躍用戶不會因固定過期時間而被強制登出 ? 提升用戶體驗&#xff1a;用戶無需頻繁重新登錄&#xff0c;只要…

Windows 中動態庫.dll 的 .lib 文件有什么作用?

在 Windows 平臺開發中, 動態鏈接庫(Dynamic Link Library, DLL)。與之相關的還有一個常讓人困惑的文件——.lib 文件。那么,這個 .lib 文件到底有什么作用呢? 一、什么是 .lib 文件? .lib 文件是 靜態導入庫(Import Library) 文件,它通常與動態鏈接庫(DLL)一起生成…

細說STM32單片機FreeRTOS消息緩沖區及其應用實例

目錄 一、消息緩沖區功能概述 二、消息緩沖區操作相關函數 1、相關函數概述 2、部分函數詳解 &#xff08;1&#xff09;創建消息緩沖區 &#xff08;2&#xff09;寫入消息 &#xff08;3&#xff09;讀取消息 &#xff08;4&#xff09;消息緩沖區狀態查詢 三、消息…

【緩存】JAVA本地緩存推薦Caffeine和Guava

&#x1f31f; 引言 在軟件開發過程中&#xff0c;緩存是提升系統性能的常用手段。對于基礎場景&#xff0c;直接使用 Java集合框架&#xff08;如Map/Set/List&#xff09;即可滿足需求。然而&#xff0c;當面對更復雜的緩存場景時&#xff1a; 需要支持多種過期策略&#x…

IDA插件 MIPSROP的安裝和使用方法

前言 筆者的IDA版本為9.0&#xff0c;剛開始根據一些博客描述以為將mipsrop.py拷貝到IDA的plugins目錄即可&#xff0c;可操作后發現事情好像沒這么簡單&#xff0c;復制進去后就發現沒有博客中所說的 MIPS ROP Finder &#xff0c;筆者在網上搜索了很多博客后在 https://bbs.…