Jeff的隨手筆記

學習當一個前端工程師

0%

『新手學習React』- React Basics_Render

接下來這篇要來看 React 產生並管理 UI 畫面的核心運作流程與原理。

如果只是單純的照著官方文件學下來不是說不會,畢竟官方文件也是花了4章節在講有關render相關的事情,但還是有些矇矇懂懂的,其實這感覺跟學JavaScript很像,功能正常畫面會出來,但問到為什麼會這樣都是答的支支吾吾,因此這次的目標就是搞懂這個Render!

條件渲染

元件通常都是需要根據不同的條件顯示不同的內容,例如我想要顯示我喜歡的影片等等。在React裡,我們可以透過JavaScript所提供的if或是運算子(operators)。

例如我想要讓畫面出現完成的打勾沒完成的保持原樣就可以這樣用:

1
2
3
4
if (isPacked) {
return <li className="item">{name} ✔</li>;
}
return <li className="item">{name}</li>;

在正常情況下都是會 return <li className="item">{name}</li>,但是如果isPackedtrue 的時候,則會 return 另一組JSX tree。

我們也可以用條件(三元)運算子(Conditional (ternary) operator)來完成這段code:

1
2
3
4
5
return (
<li className="item">
{isPacked ? name + ' ✔' : name}
</li>
);

是不是非常簡潔,但要記得不要因為嵌套條件標記而變得混亂。

我們也可以用邏輯與運算符&&):

1
2
3
4
5
return (
<li className="item">
{name} {isPacked && '✔'}
</li>
);

我們可以將其理解為“如果isPacked,則 ( &&) 呈現 ,否則,不呈現任何內容”

這邊要特別提醒,不要在 && 左側的條件不要放進數字,在 JavaScript 條件判斷時會把 0 轉成 boolean,但在 React 不會做這件事,React 會直接渲染 0 ,所以需要在 && 左邊的條件要直接放進 boolean importance > 0 && 而非 importance &&

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Item({ name, importance }) {
return (
<li className="item">
{name} {importance && <i>(Importance: {importance})</i>}
</li>
);
}

export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item importance={9} name="Space suit" />
<Item importance={0} name="Helmet with a golden leaf" />
<Item importance={6} name="Photo of Tam" />
</ul>
</section>
);
}
//Space suit Importance:9
//Helmet with a golden leaf 0
//Photo of Tam Importance:6

另外補充一種很常運用 &&||的方法 。

1
2
3
4
5
6
7
//(條件/運算式A) && (條件/運算式B)
const myVar = true && 'default value';
// myVar 的值為 "default value"

//(條件/運算式A) || (條件/運算式B)
const myVar = false || 'default value';
// myVar 的值為 "default value"

一開始看到這個我常常被搞混,但看了 Boolean 的真假判斷 之後,稍微有些理解,由於本章是複習React 所以就不深入解說,有興趣的人可以點擊連結去閱讀。

回到題目,由於我們知道只有下方這幾個值會變成false也就是「falsy」值(true 的話叫做「truthy」值):

  • Undefined
  • Null
  • +00, or NaN
  • 空字串 "" 或 ''

因此這邊我們只需要判斷 truthyfalsy 來決定要回傳哪一個。

在 && 的時候,只要第一個值為 falsy 回傳第一個值,否則回傳後者。

在 | | 的時候,只要第一個值為 truthy 回傳第一個值,否則回傳後者。

渲染列表

在官方文件裡這張主要內容是在講如何使用 map()filter()

假設我現在有一個陣列 people ,我想要把是 chemist 的資料呈現在畫面上,我們可以這樣做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const people = [{
id: 0,
name: 'Creola Katherine Johnson',
profession: 'mathematician',
}, {
id: 1,
name: 'Mario José Molina-Pasquel Henríquez',
profession: 'chemist',
}, {
id: 2,
name: 'Mohammad Abdus Salam',
profession: 'physicist',
}, {
name: 'Percy Lavon Julian',
profession: 'chemist',
}, {
name: 'Subrahmanyan Chandrasekhar',
profession: 'astrophysicist',
}];

export default function List() {

const chemists = people.filter(person =>
person.profession === 'chemist'
);

const listItems = chemists.map(person =>
<li>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
known for {person.accomplishment}
</p>
</li>
);
);
return <ul>{listItems}</ul>;
}

這樣就可以呈現出來在畫面上,但這時候我們看到錯誤:

1
Warning: Each child in a list should have a unique “key” prop.

字面上的意思是,你需要給陣列中的每一個 React 元素一個唯一的編號 (unique key),因為 React 需要知道實際上哪個元素是哪一個,React 的 Virtual DOM 才能確定哪些元素是需要在 DOM 中被新增的、哪些是要更新的、哪些是要被刪除的。

官方文件-key

另外不建議使用 index 作為key值。

如果陣列的項目沒有被分配 key,React 預設 index 作為 key。但在項目順序可能改變的情況下,官方並不建議使用 index 作為 key。原因是:

  1. 會影響效能。(這跟React預測元件更新的diffing演算法有關)
  2. 因為key是基於索引值,因此如果更動順序,也會修改當前的key,可能會導致不合預期的結果。

官方文件

官方建議的 key來源:

  • 來自數據庫的數據
  • 本地生成的數據

關於Key兩個規定:

  • key 必須在兄弟元素中是唯一的但是不同的陣列中可以使用相同的 key
  • Key 在渲染過程中不應該被生成或更改,因為這樣會破壞Key的唯一性和穩定性。

關於第二點Zet助教有特別說明:
這將導致渲染之間的 Key 永遠不會匹配,從而導致每次都重新創建所有元件和 DOM。這不僅速度慢,而且還會丟失列表項中的任何用戶輸入。相反,請使用基於數據的穩定 ID。

React 畫面更新的核心機制

前面已經說完渲染的使用方式,接下來談談底層的部分,接下來的內容都來自Zet助教鐵人賽文章,有興趣的可以前往觀看原版的。

Virtual DOM

首先我們要知道什麼是 Virtual DOM:

從意義上來說,Virtual DOM 是真實 DOM 的虛構描述體,它實際上也是一種樹狀結構的資料。

當 DOM 的節點需要更動時,不會直接修改 DOM,而是透過 DOM diff 演算法比較 Virtual DOM 修改前與修改後的樹狀結構,再批次更新真實的 DOM 節點。

每一個 Virtual DOM element 的資料都是普通的 JavaScript 物件變數,內容則嘗試在描述一個真實的 DOM element 預計要長的樣子,接著透過負責渲染畫面的程式處理後,就能將 Virtual DOM element 轉換並產生成實際的 DOM element,以更新瀏覽器的實際畫面。

所以實際流程是這樣的:

Virtual DOM (React element) => 經過 Diff Algorithm & root.render() => 產生畫面上的真實 DOM。這個過程永遠單向

在 Diff Algorithm 時,新的 Virtual DOM Tree 與此前最後一次舊畫面用的 Virtual DOM Tree 進行兩棵樹的結構細節比較,其中差異之處才是本次畫面更新中真正有需要變更的部分

至於這樣的好處就是我們能夠透過這個流程,我們就可以將真實的 DOM 操作範圍最小化,並限縮在這些真正需要變更的地方,來盡可能的減少因 DOM 操作而造成的效能花費。

更進階的內容可以參考:關於 Virtual DOM 效益的進階討論

一律重繪的渲染策略

我們了解了Virtual DOM 的概念後,接下來了解React 的一律重繪的渲染策略:

『Virtual DOM 在JavaScript 中只是一些自定義的普通變數資料而已,並不像真實 DOM 那樣有直接與瀏覽器的渲染引擎綁定,因此React才可以做出重繪整個 Virtual DOM。』

在這個策略下,React 只需要知道資料有發生變化就行,而不關心資料具體變化的差異在哪,然後進行一律重繪的流程。說的更具體一點的說,React 中一律重繪的是 React elements

因此我們在 React 中在講「渲染」或「render」時,通常都是在說 Virtual DOM elements(也就是 React elements)的產生,而不是在指真實 DOM elements 的操作。而「重繪 Virtual DOM」在 React 中通常也被稱為「re-render」,具體在 React 中的行為就是「以新的資料(props 或 state)重新再執行一次 component function,並產生新版的 React elements」。

但就算採取這個方式,當App檔案越來越龐大且複雜後,如果無止境的重繪還是會造成效能問題,因此React 會以 component 作為一律重繪的切分單位,而這也是會什麼 state 必須依附於 component function 的其中一個原因。當我們呼叫一個 state 的 setState 方法時,React 就只會重繪該 state 所屬的 component 以及它底下的所有子孫 components,而不會整個 App 都重繪。

Reconciliation

延續上一個段落,當我們在 component 裡呼叫 setState方法來觸發資料更新時,此時 React 會先以 Object.is()方法來檢查新傳入的 state 是否與舊的不同,如果相同的話則判定資料沒有變化所以畫面不用更新,就會直接中斷接下來的流程,反之可能有畫面更新的需求。

React component 會自動觸發 re-render 的流程並且透過 Virtual DOM 還有 Diff 演算法算出畫面中實際需要更新的部分,比對更新前後 virtual DOM 的差異之後,再交由react-dom負責自動去操作更新這些 DOM elements去更動真實的 DOM,有效減少渲染的次數 ,而這個 Diff 的過程也被稱作 reconciliation。

另外setState 觸發的 re-render 會連帶觸發子 components 的 re-render