iT邦幫忙

2022 iThome 鐵人賽

DAY 19
2
Modern Web

一次打破 React 常見的學習門檻與觀念誤解系列 第 19

[Day 19] 每一次 render 都有自己的 props、state 以及 event handlers

  • 分享至 

  • xImage
  •  

承接上一張節的脈絡,在我們解析大魔王 useEffect 之前,我們需要先更深入的重新梳理一下 component 生命週期的重要概念:render。


每一次 render 都有它自己的 props & state

我們先來觀察下面這個再常見不過的 counter 範例:

function Counter() {
  const [count, setCount] = useState(0);
  const handleIncrementButtonClick = () => {
    setCount(count + 1)
  };
 
  return (
    <div>
      <p>counter: {count}</p>
      <button onClick={handleIncrementButtonClick}>
        +1
      </button>
    </div>
  );
}

注意 <p>counter: {count}</p> 這行程式碼,它做了什麼事情? count 變數會「觀察」state 的變化然後自動更新嗎?這可能是剛學習 React 的初學者常見的一種直覺,不過實際上它是一種心智模型上的誤解。

在這個範例中, count 只是一個普通的數字型別變數,它既不是什麼「data binding」,也不是什麼「watcher」或「proxy」等帶有監聽性質的東西,就是一個普通的數字變數,你甚至可以這樣想像來理解:

const count = 100; // 從 useState 取出的值,是一個不會改變的常數

// ...
<p>counter: {count}</p>

到目前為止的篇幅我們已經多次提及過 render 這個概念,然而對於一個 function component 來說,「進行一次 render」具體上到底做了什麼事情?

其實答案非常單純,就是重新跑一次這個 function 而已。

因此上面的範例我們可以這樣理解:

// 在第一次 render 時
function Counter() {
  const count = 0; // 被 useState() 回傳
  // ...
  <p>counter: {count}</p>
  // ...
}

// 經過一次點擊呼叫了 setState,我們的 component function 再次被重新執行
function Counter() {
  const count = 1; // 被 useState() 回傳
  // ...
  <p>counter: {count}</p>
  // ...
}

// 經過另一次點擊呼叫了 setState,我們的 component function 又再次被重新執行
function Counter() {
  const count = 2; // 被 useState() 回傳
  // ...
  <p>counter: {count}</p>
  // ...
}

有一個重點是,在以上不同次的 render 之間,都有一個名為 count 的區域變數,但是它們在每次 render 其實是完全不相關的值,只是在 scope 內的命名相同而已。

所以我們可以下結論,這行其實不會做任何特別的 data binding 或監聽等動作:

<p>counter: {count}</p>

它只是將一個普通的數字值放進了 React element 中做為我們畫面渲染的輸出結果。結合我們在前面章節介紹過的各種核心原理的概念,就能夠很好的解釋 React 的 render 運作思維:

  • React 不會去監聽資料的變化,你必須自己主動告知 React(也就是呼叫 setState 有資料需要更新並觸發 re-render
  • re-render 做的事情就是以新版的資料(props & state)重新再執行一次 component 的 function

以上概念的關鍵點在於,在任何一次 render 裡面的 count 的值都並不會隨著時間或是呼叫 setState 而發生改變。而是每當我們呼叫 setState 時,React 會重新呼叫 component function 來重新執行一次 render。每次 render 時都會捕捉到屬於它自己版本的 count 值,這個值是個只存在於該次 render 中的常數。


每次 render 都有自己的 event handlers

現在我們已經理解每次 render 都有自己的 props 與 state 了。那麼 event handlers 呢?如果你還記得的話,其實在上一篇章中我們已經有提及這個概念,這裡讓我們用一個類似概念的範例來延伸解析:

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

  const handleIncrementButtonClick = () => {
    setCount(count + 1)
  };

  const handleAlertButtonClick = () => {
    setTimeout(() => {
      alert(`你在 counter 的值為 ${count} 時點擊了 alert 按鈕`);
    }, 3000);
  };
 
  return (
    <div>
      <p>counter: {count}</p>
      <button onClick={handleIncrementButtonClick}>
        +1
      </button>
      <button onClick={handleAlertButtonClick}>
        Show alert
      </button>
    </div>
  );
}

你可以從 這個 Codesandbox 自己動手操作看看:

  • 將 counter 的數字加到 2
  • 按下「Show alert」按鈕
  • 盡快將 counter 的數字加到 4 (在 setTimeout callback 觸發之前)
  • 觀察跳出的 alert 訊息

你覺得 alert 中顯示的數字會是 2 還是 4

讓我們來看看操作的結果:

https://i.imgur.com/vVALd02.gif

可以從上面看到,alert 的顯示結果是 2,而不會是 alert 跳出時最新的 state 值 4。這並不是因為 React 做了什麼特殊的事情才有的黑魔法,而是 JavaScript 本身的就有核心特性 closure 所導致的。

再次提醒:如果你對於 closure 的概念到底是什麼感到不確定或困惑的話,會非常建議你先搞清楚這個 JavaScript 的核心特性,然後再繼續學習 React — 因為 React 的核心概念機制幾乎到處都會依賴它。

我們目前已經了解到「每次 render 都有自己的 props 與 state,它們的值在一次 render 中是永遠不變的」了,因此當我們在 component function 中定義 event handlers 時,其實相當於這些 event handlers function 會因為 closure 的特性而「記住」它們所用到的 props 與 state:

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

const handleAlertButtonClick = () => {
  setTimeout(() => {
    // 這個 callback 會永遠記得 count 這個變數的位置,可以隨時讀取到它,
    // 而在每次 render 中 count 都是永遠不變的,不會因為 setState 而被修改
    alert(`你在 counter 的值為 ${count} 時點擊了 alert 按鈕`);
  }, 3000);
};

// ...

當 counter state 的值是 2 的時候,alert 按鈕上綁定的 event handler 事件是專屬於「count 的值是 2 的那次 render」的版本,它以 closure 記得的 count 變數永遠都會是 2。因此即使當我們點擊按鈕來增加 counter 數值後 setTimout 的 callback 才被執行,這個 callback 記得的 count 仍然是 「count 的值是 2 的那次 render」的版本:

// 模擬 count state 的值是 2 的時候的 render

const count = 2; // 從 useState 回傳

const handleAlertButtonClick = () => {
  setTimeout(() => {
    // 這個 callback 會永遠記得值是 2 的版本的 count 變數
    alert(`你在 counter 的值為 ${count} 時點擊了 alert 按鈕`);
  }, 3000);
};

// ...
// 裡面的 count 是 2 的那個版本的 handleAlertButtonClick
<button onClick={handleAlertButtonClick}>
  Show alert
</button>

// ...

這就是為什麼在 component function 裡定義的 event handlers 會「屬於」一次特定的 render:

  • 在每一次的 render 之間的 props & state 都是獨立、不互相影響的
  • 在每一次的 render 中的 props & state 永遠都會保持不變,像是該次 component function 執行時的區域常數
  • event handlers 是以原始資料(props & state)延伸出來的另一種資料結果。因此可以延伸這個概念為,每一次 render 都有他自己的 event handlers
  • 這也是為什麼當我們的 state 中有物件 / 陣列時,需要去維護並保證資料的 immutable 的原因。當事件的處理中會使用到舊 render 中的資料時,這樣才能維持每一次 render 當中的 state 都保持獨立不互相影響

參考資料

  • A Complete Guide to useEffect - Overreacted
    • 本文所講的東西蠻多的參考了這篇 Dan Abramov 的個人 blog 文章,吸收內化後再加上我自己的理解以及整理來解釋這些概念。非常推薦所有英文程度 ok 的 React 開發者去他的 blog 中閱讀原文

2024/2 更新 - 實體書平裝版本預購

在經過快要一年的努力後,本系列文的實體書版本推出了~其中新增並補充了許多鐵人賽版本中沒有的脈絡與細節,並以全彩印刷拉滿視覺上的閱讀體驗,現正熱銷中!有興趣的話歡迎參考看看,也歡迎分享給其他有接觸前端的朋友們,非常感謝大家~

《React 思維進化:一次打破常見的觀念誤解,躍升專業前端開發者》

目前首刷的軟精裝版本各大通路已經幾乎都銷售一空,接下來會再刷推出新的平裝版本:

天瓏(平裝版預購):
https://www.tenlong.com.tw/products/9786263337695

博客來(平裝版):
https://www.books.com.tw/products/0010982322

momo(平裝版):
https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=12528845


上一篇
[Day 18] Function component & class component 你可能不知道的關鍵區別
下一篇
[Day 20] 每一次 render 都有自己的 effects
系列文
一次打破 React 常見的學習門檻與觀念誤解30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
查理狐狐
iT邦新手 4 級 ‧ 2022-10-04 22:00:28

你好,文章中的 demo 連結(https://codesandbox.io/s/cranky-dirac-hkluz6?file=/src/Counter.jsx)點開後會出現 Sandbox not found,想請問是否會再更新連結 (›´ω`‹ )?

Zet iT邦新手 2 級 ‧ 2022-10-05 15:01:32 檢舉

抱歉我忘記開放觀看權限了,已打開,感謝提醒哦~

我要留言

立即登入留言