React Reconciliation
什麼是 Reconciliation
Reconciliation 是 React 中一個很重要的概念,它是 React 用來決定是否要更新 DOM 的演算法。
其中 React 做兩個假設促使演算法複雜度由 O(n^3)
降低到 O(n)
。
- 兩個不同的
type
的元件產生不同的樹狀結構 - 開發者可以透過
key
來提示哪些子元件是否被重複使用
但是什麼是 type
? 什麼是 key
? 這些都是我們在開發 React 時會遇到的問題,接下來會一一介紹。
Type
在 React 中,我們可以透過 React.createElement
來建立一個元件,其中第一個參數就是 type
。
console.log(React.createElement('div'));
或是常使用 JSX Syntax
console.log(<div />);
皆會印出下述結果
{
"type": "div",
"key": null,
"ref": null,
"props": {},
"_owner": null,
"_store": {}
}
而此輸出中的 type 就是我們所說的 type
。
type 不但可以是一個字串
同時也可以是一個 function
但是 function 必須要回傳一個 React Element
符合 React Component 的定義。
function CustomComponent() {
return <div />;
}
console.log(React.createElement(CustomComponent));
function CustomComponent() {
return <div />;
}
{
"type": ƒ CustomComponent(),
"key": null,
"ref": null,
"props": {},
"_owner": null,
"_store": {}
}
Key
在 React 中,我們可以透過 key
來提示哪些子元件是否被重複使用,這樣 React 就可以知道哪些子元件需要被更新,刪除,新增。
const element = (
<ul>
<li key="1">1</li>
<li key="2">2</li>
<li key="3">3</li>
</ul>
);
console.log(element.props.children[0]);
可看見其中的 key,也就是我們所說的 key
。
{
"type": "li",
"key": "1",
"ref": null,
"props": {
"children": "1"
},
"_owner": null,
"_store": {}
}
這裡有一項初學者常犯的錯誤
就是將 key
視為一個 prop,但是 key
並不是一個 prop
而是一個特殊的屬性,並且不會被傳遞到子元件當中
Reconciliation 如何運作
在比較更新前後差異時
React 會先比較兩個元件的 type
是否相同
如果不同的話,React 會直接刪除舊的元件,並且建立新的元件
也就代表所以原先在 Component 中的 state 都會被重置
需要注意的是 type
因為可以放入 function
所以 React 會先比較 function 是否相同
要注意的是 compare by reference
因此請盡可能避免 nested component 的發生
盡可能將 component 宣告在最定層
再來 React 會比較兩個元件的 key
是否相同
- 如果相同的話,React 會保留舊的元件,並且更新 props
- 如果不同的話,React 會刪除舊的元件,並且建立新的元件
常見開發問題
在 Render List 時,使用 index 當作 key
function DynamicForm() { const [values, setValues] = React.useState([]); const addRow = () => { const id = uuid(); setValues((prev) => [ ...prev, { id: id, }, ]); }; const removeRow = (index) => { setValues((prev) => { const valuesExcludeRemoved = prev.filter( (_v, idx) => idx !== index ); return valuesExcludeRemoved; }); }; return ( <div className="App"> <button onClick={addRow}>點我新增欄位</button> <div> {values.map((item, index) => { return ( <div className="item" key={index}> <input type="text" value={item.name} /> <button style={{ marginLeft: '10px' }} onClick={() => { removeRow(index); }} > 刪除欄位 </button> </div> ); })} </div> </div> ); }
重現步驟
- 新增 3 個 input 後
- 針對第一個 input 輸入資訊
- 並且刪除第一個 input
會發現第一個 input 的資訊會被保留
使用 index 當作 key 時,會發生 input state 重新使用的問題
原因是因為 type 相同並且 key 相同,React 就會認為是同一個元件
可以嘗試將 key 改為 unique id,就可以解決這個問題
<div className="item" key={index}>
<div className="item" key={item.id}>