iT邦幫忙

2023 iThome 鐵人賽

DAY 10
1
Modern Web

react 學習記錄系列 第 10

[Day10]我的 react 學習記錄 - react 如何運作跟 key 是什麼

  • 分享至 

  • xImage
  •  

這篇文章的主要內容

簡單說說 react 是如何運作跟 key 是什麼


react 如何運作

可以大略把 react 做的事情拆分成三個步驟,react 官方把 react 形容成在餐廳工作的服務生,三個步驟又可以類比成:

  1. Triggering a render ( 將客人的訂單送到廚房 )
  2. Rendering the component ( 在廚房準備餐點 )
  3. Committing to the DOM ( 將餐點送到桌上 )

Triggering a render

有兩種情況會觸發 render

  1. 初次渲染時初始化元件
  2. 元件或其父層元件的 state 改變

只有在上面的兩種情況下,會觸發 react 啟動元件 re-render 的動作。

Rendering the component

  • 初次渲染時 react 會呼叫根元件來做到渲染整個畫面裡的所有元件。
    createRoot(root).render(<App />);

  • 之後 react 只會針對 state 改變的元件做渲染的動作
    上一篇有提到當 state 更新的時候 react 會重新執行元件內的所有程式碼 (re-render),如果 return 的 JSX 裡面有 react 的元件的話,那個子元件也會執行相同的事情,被觸發 re-render。

React commits changes to the DOM

  • 初次渲染時 React 將透過 appendChild() 的 DOM API 將其創建的所有 DOM 節點更新到畫面上。
  • 之後 state 改變的時候 react 只會更新必要的節點,讓畫面符合最新的狀態。
    react 會在上一步 render 階段進行計算,透過計算來更新必要的部分。

virtual DOM 跟 diffing 演算法

上面有提到 react 會在 render 的階段進行計算,只更新必要的部分,那 react 要怎麼知道哪個部分是必要的呢?

這就要透過 virtual DOM 跟 diffing 演算法了。

virtual DOM

每一次 react 執行 render 的動作時 react 會創建一個稱為 virtual DOM 的樹結構,用來比較這一次的 render 跟上一次 render 有什麼不同,然後只針對不同的地方做更新。

為什麼會需要 virtual DOM 呢,因為普通的 DOM 非常巨大,每一個 node 都包含了很多複雜的屬性跟 API method,如果每次都產生一個 DOM 會讓計算的成本非常巨大,所以 react 團隊選擇使用 virtual DOM 的技術來減少計算的成本,可以透過 console 元件來觀察看看。

console.log("<App />:", <App />);

https://ithelp.ithome.com.tw/upload/images/20230918/201615836NtFQPXsDT.png

其實 virtual DOM 就是一個 JavaScript 的一個物件,裡面包含著所有 react 用來產生普通 DOM 節點所需要的元素。

透過這個內容較為單純的 virtual DOM 可以讓 react 在更新畫面時更有效率更即時。

diffing 演算法

每次 render 的時候 react 會一層一層地往下比較以下面的圖來看,當 react 比叫完發現,6 跟 7 的節點內容不同時,會保留其他本來的元素不做修改,只銷毀 6 跟 7 並且產生新的 8 跟 9。

另外有一點比較特別的,假設 6 節點本來是 <ul> 元素裡面包含了很多個 <li> 元素,假設有一個事件觸發 <ul> 變成 <ol> 元素,因為標籤改變,react 會把本來 <ul> 元素裡面所有的 <li> 全部一起移除,即使裡面的內容沒有改變,因為父層的元素改變 react 會默認裡面的所有元素也不同。

https://ithelp.ithome.com.tw/upload/images/20230918/20161583Vwj9QIImAK.jpg
圖來自: https://www.geeksforgeeks.org/what-is-diffing-algorithm/


key 的作用

當我們在 JSX 裡面用 map 或是 filter 等 Array method 來動態產生 JSX 元素時如果沒有給 key 的話會在 console 看到一個錯誤,跟你說必須要給元素一個獨一無二的 key,像這樣。

https://ithelp.ithome.com.tw/upload/images/20230918/20161583Ih1KC51qS0.png

如果你第一次遇到這個問題,很有可能會直覺的想說,array method 的第二個參數是 index,剛好也會是獨一無二的 value,可以直接把 index 當做 key 來使用。

如果有使用一些 react linter 有可能會在你的編輯器跟你說不要用 index 當作 key。

https://ithelp.ithome.com.tw/upload/images/20230918/20161583a1KkBXpupK.png

為什麼不要用 index 當做 key 呢,那是因為用 index 當作 key 有可能會有效能的問題。

上面提到 react 在每次 render 時都會進行 diffing 演算法比較 virtual DOM 上面不同的地方然後再更新到 DOM 上,如果是靜態的元素不會有任何影響,但是如果你的 JSX 元素是透過 map 或是 filter 的 method 產生時,對 react 來說每一個元素都是在 method 執行當下才產生的,所以都是全新的元素,不知道哪一些元素是本來就在畫面上的,所以才會需要給他一個 key 來協助 react 做判斷。

如果 array 裡面的資料是只會從最後面做修改、刪除或是新增的話,那影響不大,但是如果會從中間或是前面或中間做修改的話那就會有效能的影響,因為當 react 在一個一個進行比較時發現上一個 key = 0 的元素他的內容跟這一次產生的 key = 0 元素內容對不起來的時候 react 會認為這個東西不一樣,所以會重新產生新的 DOM 元素。

假設有一個 10000 筆資料的 array,在最前面新增了一筆資料,那就會導致本來的 10000 個元素的 key 跟本來的元素匹配不上,那就會全部重新產生。

但是如果每一個元素都有一個相對應的 id 當做他們的 key 的話匹配就不會有問題,當 key 跟元素對得上的時候就知道這個元素只是移動位置跟排序而已並不需要重新產生。

用 index 當 key 還會有另外一個問題,就是元素內狀態有可能會錯置。

假設我有一個 nameList,並且透過 setState 在最前面加上一個名字如下,假設 map 出來的元素都有各自的狀態,這邊用 input 做範例。

const name = ["Evan", "Ruby"];

function App() {
  const [nameList, setNameList] = useState(name);

  function addName() {
    setNameList(["Roy", ...nameList]);
  }

  return (
    <div>
      <p>Here is input list:</p>
      {nameList.map((name, index) => (
        <div key={index}>
          <label>
            name:{name}
            <input type="text" />
          </label>
        </div>
      ))}
      <button onClick={addName}>add name</button>
    </div>
  );
}

當使用 index 作為 key 的時候就會出現下面的狀況。

key1

當我點擊新增的時候出現了一個奇怪的狀況,在 Evan input 的 value 跑到了 Roy 的 input,而 Ruby input 的 value 則跑到了 Evan 的 input。

這是因為 react 的 diffing 演算法用標籤作為單位比較,標籤的 key 跟內容對不上的時候會重新產生 map 出來的標籤,但是又會發現裡面的 input 並沒有改變,virtual DOM 會把裡面的 input 標籤跟他的狀態保留下來,然後依照本來的順序放回 DOM 裡面,所以才會出現這個狀態。

那當我們給他一個獨一無二的 id 時又會怎麼樣呢?

const name = [
	// 每個 item 上加上 id 元素
  { name: "Evan", id: "1" },
  { name: "Ruby", id: "2" },
];

function App() {
  const [nameList, setNameList] = useState(name);

  function addName() {
    setNameList([{ name: "Roy", id: "3" }, ...nameList]);
  }

  return (
    <div>
      <p>Here is input list:</p>
      {nameList.map(({ name, id }) => (
        <div key={id}>
          <label>
            name:{name}
            <input type="text" />
          </label>
        </div>
      ))}
      <button onClick={addName}>add name</button>
    </div>
  );
}

在每個 item 上新增了 id 屬性。

key2

當在 nameList 的最前面新增了一個項目,其他 input 的狀態也被正確的保留下來了。

因為當 key 跟標籤內的內容是相對應的時候 react 就會知道,元素只是改變位置而已,不需要重新產生新的元素。


Render and Commit - react document
Rendering Lists - react document
Reconciliation - react document(old)
What is Diffing Algorithm ? - geeksforgeeks

下一篇會簡單介紹另一個常用的 react hook - useEffect
如果內容有誤再麻煩大家指教,我會盡快修改。

這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium


上一篇
[Day9]我的 react 學習記錄 - react event 綁定 & useState
下一篇
[Day11]我的 react 學習記錄 - useEffect
系列文
react 學習記錄30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言