React 用 key 強迫 component 重新渲染


Posted by MoreCoke on 2021-04-29

這應該不是好做法,因為在官方文件上並沒有看到相關的用法,不過這用法很方便就是...。
除了 React 外,Vue 也能這樣玩,Angular 沒用過不知道。

key 的用法

我們經常會用陣列的資料搭配 map 來做出多個 component 列表,並在這些 component 都加上 key 用來辨識,日後重新 render 列表時 React 就能透過這些 key 來追蹤哪些 component 沒動過,哪些有被改到,決定是否要保留現有的 DOM 元素要對它更新就好,還是說要砍掉該 DOM 元素再另外建個新的,進而提升渲染的效率。

為什麼能提升效率?

因為在重新 render 時 React 會比較更新過的 state 資料,來更新 Virtual DOM tree 最後才去改 DOM tree,會使用遞迴去檢查內部的東西有沒有改變,這時就可以透過 key 進行初步判斷,先快速找到新舊陣列間兩個新舊元素之間的關聯,最後再將差異反映在 DOM 元素上,而不是每次重新渲染直接把 DOM 元素刪掉後再重做個新的。

// 假設改資料前的陣列是 => [{name:'apple',id:'a'},{name:'banana',id:'b'}]
// 透過 setState 改陣列資料後是 => [{name:'apple',id:'a'},{name:'pineapple',id:'p'}]
<ul>
 {
   array.map(e=><li key={e.id}>{e.name}</li>)
 }
</ul>

DOM 的前後變化:

// 改資料前的樣子
<ul>
  <li key='a'>apple</li>
  <li key='banana'>banana</li>
</ul>

// 改資料後的樣子
<ul>
  <li key='a'>apple</li>
  <li key='p'>pineapple</li>
</ul>

如果沒有 key,React 看到上面這情況會使用遞迴去看 li 中的內容(children, 也就是 apple 和 banana)所對應到陣列中的哪個元素,有了 key 的幫忙可以更快找到新舊 DOM 元素之間的關係,可以更快知道 key='a' 這值沒變,直接把舊的 Virtual DOM tree 上的 <li key='a'>apple</li> 移到新的 Virtual DOM tree 就好; key='b' 不見了,舊的 Virtual DOM tree 上的 <li key='b'>banana</li> 就不會移到新的 Virtual DOM tree;多了一個新的 key='p',在新的 Virtual DOM tree 上建一個新的 <li key='p'>pineapple</li>

key 的更動代表什麼?

相信大家都知道,如果map 出來的列表是會變動的,key 不要綁索引值,這樣 key 無法讓新舊的 Virtual DOM 元素產生對應, key 的變動都會讓 DOM 元素新增或刪除。

  • 多了新的 key: 建立一個新的 DOM 元素。
  • 少了一個 key: 把跟該 key 相關的 DOM 元素刪掉。

用 key 強迫更新

其實任何 DOM 元素都能加 key,利用更新 key 的方式就能重新 render 該 DOM 元素。當我們更動 key 就相當於:

  • 舊的 key 不見了 => React 把這個 DOM 元素刪掉(unmount)。
  • 發現新的 key => React 替這個 key 新增一個 DOM 元素(mount)。

範例

我今天有個 video,我想在這個 video播完影片後就去播下個影片,我可以怎麼做?

我在影片結束時呼叫 onEnded 用來播下個影片,在 source 插入動態連結,這樣有個問題,第一個影片播完後就不會播了,雖然 source 有變,但 react 繼續復用 video 這個 DOM 導致影片連結更新但沒有反應。

const videoNames = [
  "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
  "https://www.w3schools.com/tags/movie.ogg"
];

export default function StandbyVideo() {
  const [videoIdx, setVideoIdx] = useState(0);
  const playNextVideo = () => {
    const nextVideoIdx = (videoIdx + 1) % videoNames.length;
    setVideoIdx(nextVideoIdx);
  };
  return (
    <div>
      <video
        className={css(styles.videoContainer)}
        autoPlay={true}
        onEnded={playNextVideo}
        controls
      >
        <source src={videoNames[videoIdx]} />
      </video>
    </div>
  );
}

加入 key 後就能解決問題了,這樣的意思是每次我的 videoIdx 變了,React 就會把舊的 video 給刪掉(unmount),再重生一個 video (mount)。

<video
  key={videoIdx}
  className={css(styles.videoContainer)}
  autoPlay={true}
  onEnded={playNextVideo}
  controls
>
  <source src={videoNames[videoIdx]} />
</video>

題外話,不用 source 直接將 src 寫在 video 上也能解決需求,因為我想展示 key 重新 render 的效果,所以才這樣寫。

<video
  className={css(styles.videoContainer)}
  autoPlay={true}
  onEnded={playNextVideo}
  src={videoNames[videoIdx]}
  controls
>
</video>

附上 codesnadbox

參考資料

Virtual Dom && Diff原理,极简版
[Day 04] 理解React Virtual DOM
Reconciliation
Virtual DOM and Internals


#React #js







Related Posts

[前端工具] 從一張圖理解 Webpack 到底在幹嘛

[前端工具] 從一張圖理解 Webpack 到底在幹嘛

methods、computed與watch的不同

methods、computed與watch的不同

npm and eslint

npm and eslint


Comments