跳至正文

🎨 什麼是 Virtual DOM?

📖 定義

Virtual DOM 是實際 DOM 的輕量級複製品,是一個僅存在於記憶體中的 JavaScript 物件。像 React 這樣的函式庫會使用 Virtual DOM 來先虛擬處理 UI 的變更,然後只對實際 DOM 應用最小的變更。透過這種方式,大幅提升網頁應用程式的效能。

🎯 透過比喻理解

建築設計圖比喻

將 Virtual DOM 比喻成建築設計圖:

實際建築 (Real DOM)
├─ 建築成本高昂
├─ 修改耗時
└─ 拆除重建困難

設計圖 (Virtual DOM)
├─ 可在紙上自由修改
├─ 快速且便宜
├─ 可比較多種方案
└─ 只將最終方案反映到實際建築

流程:
1. 在設計圖上多次修改 (Virtual DOM)
2. 找出變更的部分
3. 僅將最小變更應用到實際建築 (Real DOM)

如此可節省建築成本和時間!

文字處理器比喻

記事本 (原生 JavaScript)
- 每次輸入立即儲存
- 緩慢且效率低
- 重複檔案讀寫

文字處理器 (React + Virtual DOM)
- 在記憶體中編輯
- 追蹤變更
- 只在按下儲存時寫入檔案
- 快速且高效

Virtual DOM = 記憶體緩衝區
Real DOM = 儲存在硬碟

遊戲渲染比喻

遊戲開發中:

直接在螢幕上繪製 (直接操作 Real DOM)
- 出現閃爍
- 效能下降
- 難以部分更新

雙重緩衝 (Virtual DOM 方式)
1. 在背景緩衝區繪製圖像
2. 完成後替換前景螢幕
3. 平滑渲染
4. 僅更新變更的像素

Virtual DOM 扮演與雙重緩衝相同的角色!

⚙️ 運作原理

1. 什麼是 DOM?

// DOM (Document Object Model)
// HTML 可以被 JavaScript 操作的介面

<!DOCTYPE html>
<html>
<body>
<div id="root">
<h1>Hello</h1>
<p>World</p>
</div>
</body>
</html>

// DOM 樹
document
└─ html
└─ body
└─ div#root
├─ h1 ("Hello")
└─ p ("World")

// 使用 JavaScript 操作 DOM
document.getElementById('root').innerHTML = '<h1>Changed!</h1>';

2. Real DOM 的問題點

// ❌ 低效的 Real DOM 操作

// 問題 1:每次都全部重新渲染
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
document.body.appendChild(div); // 操作 DOM 1000 次!
}

// 問題 2:重新計算佈局 (Reflow)
element.style.width = '100px'; // 發生 Reflow
element.style.height = '100px'; // 發生 Reflow
element.style.margin = '10px'; // 發生 Reflow

// 問題 3:複雜的 UI 更新
// 即使只有一個字元改變,也要重新建立整個元件
<div>
<Header />
<Content /> ← 只有這個變更
<Footer />
</div>
// 需要重新建立全部!

// Real DOM 操作緩慢的原因:
// 1. 瀏覽器渲染引擎和 JavaScript 引擎之間的通訊
// 2. Reflow(重新計算佈局)
// 3. Repaint(重新繪製螢幕)

3. Virtual DOM 作業流程

// ✅ Virtual DOM 的有效處理

// 第 1 階段:初始渲染
const initialVirtualDOM = {
type: 'div',
props: { id: 'app' },
children: [
{ type: 'h1', props: {}, children: ['Count: 0'] },
{ type: 'button', props: {}, children: ['Click'] }
]
};

// 反映到 Real DOM
<div id="app">
<h1>Count: 0</h1>
<button>Click</button>
</div>

// 第 2 階段:狀態變更 (count = 1)
const newVirtualDOM = {
type: 'div',
props: { id: 'app' },
children: [
{ type: 'h1', props: {}, children: ['Count: 1'] }, // ← 已變更
{ type: 'button', props: {}, children: ['Click'] }
]
};

// 第 3 階段:Diffing(計算差異)
const diff = {
path: ['div', 'h1', 'text'],
oldValue: 'Count: 0',
newValue: 'Count: 1'
};

// 第 4 階段:Reconciliation(調和)
// 只對 Real DOM 應用最小變更
document.querySelector('h1').textContent = 'Count: 1';
// div 和 button 保持不變!

4. Diffing 演算法

// React 的 Diffing 演算法原理

// 規則 1:不同類型的元素會建立不同的樹
// 之前
<div>
<Counter />
</div>

// 之後
<span>
<Counter />
</span>

// 結果:將 div 完全替換為 span
// Counter 也會被卸載後重新掛載

// 規則 2:使用 key 追蹤清單項目
// ❌ 沒有 key
<ul>
<li>A</li>
<li>B</li>
</ul>

// 在最前方新增 C
<ul>
<li>C</li> // 重新建立全部
<li>A</li>
<li>B</li>
</ul>

// ✅ 使用 key
<ul>
<li key="a">A</li>
<li key="b">B</li>
</ul>

// 在最前方新增 C
<ul>
<li key="c">C</li> // 只新增 C
<li key="a">A</li> // 保持不變
<li key="b">B</li> // 保持不變
</ul>

// 規則 3:遞迴比較子節點
function diff(oldNode, newNode) {
// 1. 如果類型不同,則替換
if (oldNode.type !== newNode.type) {
return { action: 'REPLACE', newNode };
}

// 2. 比較屬性
const propsDiff = diffProps(oldNode.props, newNode.props);

// 3. 比較子節點(遞迴)
const childrenDiff = diffChildren(
oldNode.children,
newNode.children
);

return { propsDiff, childrenDiff };
}

5. Reconciliation(調和)

// Reconciliation:將 Virtual DOM 反映到 Real DOM

// 階段 1:渲染階段(非同步)
// - 比較 Virtual DOM
// - 計算變更
// - 可中斷(React 18 Fiber)

// 階段 2:提交階段(同步)
// - 更新 Real DOM
// - 執行生命週期方法
// - 不可中斷

// 範例:更新清單
const oldList = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
{ id: 3, name: 'C' }
];

const newList = [
{ id: 1, name: 'A' },
{ id: 3, name: 'C-updated' }, // 修改
{ id: 4, name: 'D' } // 新增
// id: 2 刪除
];

// Reconciliation 結果:
// - id:1 保持不變
// - id:2 移除
// - id:3 只更新文字
// - id:4 新增

// 最小化實際 DOM 操作:
// remove(id:2)
// updateText(id:3, 'C-updated')
// append(id:4)

💡 實際範例

基本 Virtual DOM 實現

// 簡單的 Virtual DOM 實現(教學用)

// 1. 建立 Virtual DOM 節點
function createElement(type, props = {}, ...children) {
return {
type,
props,
children: children.flat()
};
}

// 不使用 JSX
const vdom = createElement(
'div',
{ id: 'app', className: 'container' },
createElement('h1', {}, 'Hello Virtual DOM'),
createElement('p', {}, 'This is a paragraph')
);

console.log(vdom);
// {
// type: 'div',
// props: { id: 'app', className: 'container' },
// children: [
// { type: 'h1', props: {}, children: ['Hello Virtual DOM'] },
// { type: 'p', props: {}, children: ['This is a paragraph'] }
// ]
// }

// 2. 將 Virtual DOM 轉換為 Real DOM
function render(vnode) {
// 文字節點
if (typeof vnode === 'string' || typeof vnode === 'number') {
return document.createTextNode(vnode);
}

// 元素節點
const element = document.createElement(vnode.type);

// 應用屬性
Object.entries(vnode.props || {}).forEach(([key, value]) => {
if (key === 'className') {
element.setAttribute('class', value);
} else if (key.startsWith('on')) {
// 事件監聽器
const event = key.substring(2).toLowerCase();
element.addEventListener(event, value);
} else {
element.setAttribute(key, value);
}
});

// 遞迴渲染子節點
(vnode.children || []).forEach(child => {
element.appendChild(render(child));
});

return element;
}

// 使用
const domElement = render(vdom);
document.body.appendChild(domElement);

Diffing 演算法實現

// 3. Diffing 演算法(簡化版)
function diff(oldVNode, newVNode) {
// 1. 如果新節點不存在,則移除
if (!newVNode) {
return { type: 'REMOVE' };
}

// 2. 如果舊節點不存在,則新增
if (!oldVNode) {
return { type: 'CREATE', newVNode };
}

// 3. 如果類型不同,則替換
if (
typeof oldVNode !== typeof newVNode ||
(typeof oldVNode === 'string' && oldVNode !== newVNode) ||
oldVNode.type !== newVNode.type
) {
return { type: 'REPLACE', newVNode };
}

// 4. 如果是相同類型,則更新
if (newVNode.type) {
return {
type: 'UPDATE',
props: diffProps(oldVNode.props, newVNode.props),
children: diffChildren(oldVNode.children, newVNode.children)
};
}

// 5. 無變更
return { type: 'NONE' };
}

// 比較屬性
function diffProps(oldProps = {}, newProps = {}) {
const patches = [];
```javascript
// 變更或新增的屬性
Object.keys(newProps).forEach(key => {
if (oldProps[key] !== newProps[key]) {
patches.push({ type: 'SET_PROP', key, value: newProps[key] });
}
});

// 移除的屬性
Object.keys(oldProps).forEach(key => {
if (!(key in newProps)) {
patches.push({ type: 'REMOVE_PROP', key });
}
});

return patches;
}

// 比較子節點
function diffChildren(oldChildren = [], newChildren = []) {
const patches = [];
const maxLength = Math.max(oldChildren.length, newChildren.length);

for (let i = 0; i < maxLength; i++) {
patches.push(diff(oldChildren[i], newChildren[i]));
}

return patches;
}

應用 Patch

// 4. 將 Patch 應用到 Real DOM
function patch(parent, patches, index = 0) {
if (!patches) return;

const element = parent.childNodes[index];

switch (patches.type) {
case 'CREATE':
parent.appendChild(render(patches.newVNode));
break;

case 'REMOVE':
parent.removeChild(element);
break;

case 'REPLACE':
parent.replaceChild(render(patches.newVNode), element);
break;

case 'UPDATE':
// 更新屬性
patches.props.forEach(propPatch => {
if (propPatch.type === 'SET_PROP') {
element.setAttribute(propPatch.key, propPatch.value);
} else if (propPatch.type === 'REMOVE_PROP') {
element.removeAttribute(propPatch.key);
}
});

// 遞迴更新子節點
patches.children.forEach((childPatch, i) => {
patch(element, childPatch, i);
});
break;
}
}

React 中使用 Virtual DOM

import React, { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

// React 自動處理 Virtual DOM
return (
<div className="counter">
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>
遞增
</button>
<button onClick={() => setCount(count - 1)}>
遞減
</button>
</div>
);
}

// 內部運作:
// 1. count 變更 → setState 呼叫
// 2. React 建立新的 Virtual DOM
// 3. 與先前的 Virtual DOM 比較(Diffing)
// 4. 偵測到 <h1> 文字已變更
// 5. 只更新 Real DOM 的文字節點
// 6. div 和 button 保持不變

// 性能比較:
// ❌ 原生 JS:替換整個 div.innerHTML
// ✅ React:只更新 <h1> 的文字

複雜列表範例

import React, { useState } from 'react';

function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '運動', done: false },
{ id: 2, text: '學習', done: false },
{ id: 3, text: '打掃', done: false }
]);

const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};

const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};

return (
<div>
<h1>待辦事項清單</h1>
<ul>
{todos.map(todo => (
<li
key={todo.id} // key 對於 Diffing 優化是必要的!
style={{
textDecoration: todo.done ? 'line-through' : 'none',
color: todo.done ? '#888' : '#000'
}}
>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>刪除</button>
</li>
))}
</ul>
</div>
);
}

// Virtual DOM 的效率:
// 勾選一個 todo 時
// - ❌ 不重新生成整個清單 (X)
// - ✅ 只變更該 <li> 的樣式 (O)

// key 的重要性:
// 有 key 時:
// - 項目移動/刪除時更有效率
// - 可重複使用 DOM
// - 保持元件狀態

// 沒有 key 時:
// - 重新生成整個清單
// - 效能降低
// - 可能遺失元件狀態

效能最佳化範例

import React, { useState, useMemo, memo } from 'react';

// 使用 memo 對元件進行記憶化
const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) {
console.log(`TodoItem ${todo.id} 渲染`);

return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>刪除</button>
</li>
);
});

function OptimizedTodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '運動', done: false },
{ id: 2, text: '學習', done: false },
{ id: 3, text: '打掃', done: false }
]);

// 使用 useMemo 快取計算結果
const activeTodos = useMemo(() => {
console.log('計算活躍的待辦事項');
return todos.filter(todo => !todo.done);
}, [todos]);

const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};

const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};

return (
<div>
<h1>待辦事項清單</h1>
<p>剩餘待辦事項:{activeTodos.length}</p>
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</ul>
</div>
);
}

// 最佳化效果:
// 1. memo:變更一個 todo 時,只重新渲染該 TodoItem
// 2. useMemo:只在 todos 變更時重新計算 activeTodos
// 3. key:高效更新清單

// 性能比較:
// 最佳化前:10個 todo 中變更 1 個 → 重新渲染所有 10 個
// 最佳化後:10個 todo 中變更 1 個 → 只渲染 1 個

有無 Virtual DOM 的比較

// ❌ 沒有 Virtual DOM(原生 JavaScript)
function updateWithoutVirtualDOM() {
const app = document.getElementById('app');
const count = 0;

// 每次重新生成整個 HTML
setInterval(() => {
count++;
app.innerHTML = `
<div class="container">
<h1>Count: ${count}</h1>
<p>當前時間:${new Date().toLocaleTimeString()}</p>
<button>點擊我</button>
</div>
`;
// 問題:
// 1. 重新生成整個 DOM 樹
// 2. 事件監聽器遺失
// 3. 輸入中的 input 被初始化
// 4. 捲軸位置被初始化
// 5. 動畫中斷
}, 1000);
}

// ✅ 有 Virtual DOM(React)
function CounterWithVirtualDOM() {
const [count, setCount] = useState(0);
const [time, setTime] = useState(new Date());

useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
setTime(new Date());
}, 1000);

return () => clearInterval(timer);
}, []);

return (
<div className="container">
<h1>Count: {count}</h1>
<p>當前時間:{time.toLocaleTimeString()}</p>
<button onClick={() => alert('已點擊!')}>點擊我</button>
</div>
);
}

// 優點:
// 1. 只更新 <h1> 和 <p> 的文字
// 2. 保持事件監聽器
// 3. 保持元件狀態
// 4. 平順的更新
// 5. 最小化 DOM 操作

實際效能測量

import React, { useState, useEffect } from 'react';

function PerformanceComparison() {
const [items, setItems] = useState([]);
const [renderTime, setRenderTime] = useState(0);

const generateItems = (count) => {
const startTime = performance.now();

const newItems = Array.from({ length: count }, (_, i) => ({
id: i,
value: Math.random()
}));

setItems(newItems);

const endTime = performance.now();
setRenderTime(endTime - startTime);
};

return (
<div>
<h1>效能比較</h1>
<div>
<button onClick={() => generateItems(100)}>
生成 100 個
</button>
<button onClick={() => generateItems(1000)}>
生成 1000 個
</button>
<button onClick={() => generateItems(10000)}>
生成 10000 個
</button>
</div>

<p>渲染時間:{renderTime.toFixed(2)}ms</p>

<div style={{ maxHeight: '400px', overflow: 'auto' }}>
{items.map(item => (
<div key={item.id} style={{ padding: '5px', borderBottom: '1px solid #eee' }}>
項目 #{item.id}: {item.value.toFixed(4)}
</div>
))}
</div>
</div>
);
}

// 結果(一般情況):
// 原生 JS(整個 innerHTML):
// - 100 個:~50ms
// - 1000 個:~500ms
// - 10000 個:~5000ms(卡頓)

// React(Virtual DOM):
// - 100 個:~10ms
// - 1000 個:~100ms
// - 10000 個:~1000ms(平順)

// Virtual DOM 的性能提升:
// 初始渲染相似,
// 但更新時快 5-10 倍!

使用 Chrome DevTools 確認

// React DevTools Profiler 使用方法

// 1. 安裝 React DevTools
// 在 Chrome 網路商店中安裝 "React Developer Tools"

// 2. 開啟 Profiler 標籤
// - 點擊錄製按鈕
// - 在應用程式中執行操作
// - 停止錄製

// 3. 分析結果
// - Flamegraph:元件渲染時間
// - Ranked:渲染時間排名
// - 點擊元件:確認為什麼重新渲染

function ProfiledComponent() {
const [count, setCount] = useState(0);

// 使用 Profiler 包裝以測量渲染效能
return (
<React.Profiler
id="Counter"
onRender={(id, phase, actualDuration) => {
console.log(`${id} (${phase}) 耗時 ${actualDuration}ms`);
}}
>
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
</React.Profiler>
);
}

// 主控台輸出:
// Counter (掛載) 花費 2.3ms
// Counter (更新) 花費 0.5ms ← 感謝 Virtual DOM 的快速效能!

🤔 常見問題

Q1. Virtual DOM 是否永遠比 Real DOM 快?

答: 不是,這取決於不同的情況:

// ✅ Virtual DOM 速度快的情況:
// 1. 複雜的 UI 更新
function ComplexUI() {
const [data, setData] = useState([...]);

// 許多元件中只有部分變更
return (
<div>
{data.map(item => <ComplexComponent key={item.id} {...item} />)}
</div>
);
// Virtual DOM:只更新變更的部分
// Real DOM:需要重新生成整個內容
}

// 2. 頻繁更新
function LiveCounter() {
const [count, setCount] = useState(0);

useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 100); // 每秒更新 10 次
return () => clearInterval(timer);
}, []);

return <h1>Count: {count}</h1>;
// Virtual DOM:只變更文字
// Real DOM:每次都要操作 DOM
}

// ❌ Virtual DOM 較慢的情況:
// 1. 簡單的 UI(有額外開銷)
function SimpleButton() {
return <button>Click</button>;
}
// Virtual DOM 比較 + Real DOM 更新
// vs
// 直接操作 Real DOM

// 2. 初始渲染
// 建立 Virtual DOM + Diffing + 建立 Real DOM
// vs
// 直接建立 Real DOM

// 結論:
// Virtual DOM 的優勢在於「更新效能」
// 初始載入可能稍微慢一些,
// 但在複雜的應用程式中整體上更快!

Q2. 為什麼 key 很重要?

答: key 是 React 用來識別哪些項目被變更、新增或刪除的:

// ❌ 沒有 key(效率低)
function BadList() {
const [items, setItems] = useState(['A', 'B', 'C']);

return (
<ul>
{items.map(item => <li>{item}</li>)}
</ul>
);
}

// 在前面新增 'D':
// 之前:<li>A</li> <li>B</li> <li>C</li>
// 之後:<li>D</li> <li>A</li> <li>B</li> <li>C</li>

// 當 React 沒有 key 時:
// - 第 0 個:A → D(變更)
// - 第 1 個:B → A(變更)
// - 第 2 個:C → B(變更)
// - 第 3 個:無 → C(新增)
// 結果:全部 4 個都更新!(低效)

// ✅ 使用 key(高效)
function GoodList() {
const [items, setItems] = useState([
{ id: 'a', text: 'A' },
{ id: 'b', text: 'B' },
{ id: 'c', text: 'C' }
]);

return (
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}

// 在前面新增 'D':
// 之前:key=a, key=b, key=c
// 之後:key=d, key=a, key=b, key=c

// React 通過 key 識別:
// - key=a:保持不變
// - key=b:保持不變
// - key=c:保持不變
// - key=d:新增
// 結果:只新增 1 個!(高效)

// key 規則:
// 1. 在兄弟節點間必須唯一
// 2. 不應改變
// 3. 必須可預測

// ❌ 不好的 key 範例:
{items.map((item, index) => <li key={index}>{item}</li>)}
// 索引改變會出問題!

{items.map(item => <li key={Math.random()}>{item}</li>)}
// 每次都產生新 key → 始終重新生成!

// ✅ 好的 key 範例:
{items.map(item => <li key={item.id}>{item}</li>)}
// 使用資料的唯一 ID

{items.map(item => <li key={item.email}>{item}</li>)}
// 使用唯一屬性

Q3. 可以直接操作 Virtual DOM 嗎?

答: 不行,React 會自動處理。但可以進行優化:

import React, { useState, useMemo, useCallback, memo } from 'react';

// 1. React.memo - 元件記憶化
const ExpensiveComponent = memo(function ExpensiveComponent({ value }) {
console.log('ExpensiveComponent 渲染');

// 複雜計算
const result = useMemo(() => {
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
return sum + value;
}, [value]);

return <div>Result: {result}</div>;
});

// 如果 props 相同,跳過重新渲染!

// 2. useMemo - 值記憶化
function ListComponent({ items }) {
// 只在 items 變更時重新計算
const sortedItems = useMemo(() => {
console.log('排序中...');
return [...items].sort((a, b) => a.value - b.value);
}, [items]);

return (
<ul>
{sortedItems.map(item => (
<li key={item.id}>{item.value}</li>
))}
</ul>
);
}

// 3. useCallback - 函式記憶化
function Parent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);

// 只在 count 變更時建立新函式
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);

return (
<div>
<Child onClick={handleClick} />
<button onClick={() => setOther(other + 1)}>
Other: {other}
</button>
</div>
);
}

const Child = memo(function Child({ onClick }) {
console.log('Child 渲染');
return <button onClick={onClick}>Click</button>;
});

// other 變更時 Child 不會重新渲染!

// 4. shouldComponentUpdate / PureComponent(類別元件)
class OptimizedComponent extends React.PureComponent {
render() {
return <div>{this.props.value}</div>;
}
}

// 如果 props 未變更,不會重新渲染

// 5. 條件渲染優化
function ConditionalRendering({ showDetails, basicInfo, detailInfo }) {
return (
<div>
<BasicInfo data={basicInfo} />
{showDetails && <DetailInfo data={detailInfo} />}
</div>
);
}

// 如果 showDetails 為 false,DetailInfo 甚至不會出現在 Virtual DOM 中!

Q4. 有不使用 Virtual DOM 的框架嗎?

答: 是的,像 Svelte 這樣的框架使用不同的方法:

// React(使用 Virtual DOM)
function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}

// 運行時:
// 1. 建立 Virtual DOM
// 2. Diffing
// 3. 更新 Real DOM

// Svelte(編譯時優化)
<script>
let count = 0;
</script>

<div>
<h1>Count: {count}</h1>
<button on:click={() => count += 1}>+</button>
</div>

// 編譯結果:
function update_count() {
h1.textContent = `Count: ${count}`; // 直接操作 DOM!
}

// 優缺點比較:

// Virtual DOM(React, Vue)
// 優點:
// - 宣告式 UI
// - 可預測的效能
// - 豐富的生態系統

// 缺點:
// - 記憶體額外開銷
// - Diffing 成本
// - 打包大小大

// 無 Virtual DOM(Svelte)
// 優點:
// - 高效能
// - 小型打包大小
// - 低記憶體使用

// 缺點:
// - 生態系統小
// - 複雜應用程式?
// - 學習曲線

// Solid.js(細粒度響應性)
// 無 Virtual DOM,使用 React 語法
import { createSignal } from 'solid-js';

function Counter() {
const [count, setCount] = createSignal(0);

return (
<div>
<h1>Count: {count()}</h1>
<button onClick={() => setCount(count() + 1)}>+</button>
</div>
);
}

// 只精確更新變更的部分!
// 無 Virtual DOM 也很高效!

Q5. React 18 的 Concurrent Rendering 是什麼?

答: 是可以中斷和恢復 Virtual DOM 更新的功能:

import React, { useState, useTransition, Suspense } from 'react';

// 1. useTransition - 低優先級更新
function SearchBox() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();

const handleChange = (e) => {
// 立即更新(高優先級)
setQuery(e.target.value);

// 稍後更新(低優先級)
startTransition(() => {
// 高開銷計算
const newResults = expensiveSearch(e.target.value);
setResults(newResults);
});
};

return (
<div>
<input
value={query}
onChange={handleChange}
placeholder="搜尋..."
/>
{isPending && <span>搜尋中...</span>}
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
}

// 運作方式:
// 1. 使用者快速輸入
// 2. query 立即更新(保持 input 響應性)
// 3. results 緩慢更新(背景)
// 4. 收到新輸入時中斷先前 results 更新!

// 2. Suspense - 載入狀態管理
const LazyComponent = React.lazy(() => import('./Heavy'));

function App() {
return (
<Suspense fallback={<div>載入中...</div>}>
<LazyComponent />
</Suspense>
);
}

// 3. useDeferredValue - 延遲值
function FilteredList({ items, query }) {
const deferredQuery = useDeferredValue(query);

const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.includes(deferredQuery)
);
}, [items, deferredQuery]);

return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}

// Concurrent Rendering 的優點:
// 1. 保持響應性:UI 不會卡頓
// 2. 優先級:重要更新優先
// 3. 可中斷:跳過不必要的工作
// 4. 平滑過渡:避免突然變化

// 內部運作:
// 時間分片:
// [輸入] [渲染] [輸入] [渲染] ...
// 將工作分成多個小片段執行

// 優先級:
// Urgent(立即):點擊、輸入、懸停
// Transition(稍後):搜尋結果、篩選
// Deferred(最晚):分析、日誌記錄

🎓 下一步

如果已經理解 Virtual DOM,建議學習:

  1. 什麼是 React?(即將撰寫文件) - 使用 Virtual DOM 的代表性函式庫
  2. CSR vs SSR vs SSG - 渲染策略比較
  3. 網路效能最佳化(即將撰寫文件) - 利用 Virtual DOM 進行最佳化

🎬 結語

Virtual DOM 是現代 Web 框架的核心技術:

  • 概念:Real DOM 的輕量副本
  • 原理:通過 Diffing 計算變更並進行最小更新
  • 優點:提升複雜 UI 更新效能
  • 核心:透過宣告式程式設計改善開發者體驗

感謝 Virtual DOM,我們可以專注於 UI 邏輯而無需擔心效能!