承接上一張節的脈絡,在我們解析大魔王 useEffect
之前,我們需要先更深入的重新梳理一下 component 生命週期的重要概念:render。
我們先來觀察下面這個再常見不過的 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 運作思維:
setState
) 有資料需要更新並觸發 re-render以上概念的關鍵點在於,在任何一次 render 裡面的 count
的值都並不會隨著時間或是呼叫 setState
而發生改變。而是每當我們呼叫 setState
時,React 會重新呼叫 component function 來重新執行一次 render。每次 render 時都會捕捉到屬於它自己版本的 count
值,這個值是個只存在於該次 render 中的常數。
現在我們已經理解每次 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 自己動手操作看看:
2
4
(在 setTimeout
callback 觸發之前)你覺得 alert 中顯示的數字會是 2
還是 4
?
讓我們來看看操作的結果:
可以從上面看到,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:
在經過快要一年的努力後,本系列文的實體書版本推出了~其中新增並補充了許多鐵人賽版本中沒有的脈絡與細節,並以全彩印刷拉滿視覺上的閱讀體驗,現正熱銷中!有興趣的話歡迎參考看看,也歡迎分享給其他有接觸前端的朋友們,非常感謝大家~
《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
你好,文章中的 demo 連結(https://codesandbox.io/s/cranky-dirac-hkluz6?file=/src/Counter.jsx
)點開後會出現 Sandbox not found,想請問是否會再更新連結 (›´ω`‹ )?
抱歉我忘記開放觀看權限了,已打開,感謝提醒哦~