希望看完以後你會對 React 函數組件有更深入的了解。
場景復現整個 Demo 非常簡單,大家可以自己在電腦上嘗試一下。
首先,有一個 button 和一個 list:
<divclassName="App"><buttononClick={add}>Add</button>{list.map(val=>val)}</div>list 是使用 useState 管理的狀態。button 綁定了事件 onClick={add}。
點擊按鈕,會執行 add 方法向 list 中加入一些內容。
exportdefaultfunctionApp(){const[list,setList]=useState([]);constadd=()=>{//...};return(<divclassName="App"><buttononClick={add}>Add</button>{list.map(val=>val)}</div>);}現在頁面看起來像這樣:

我們繼續,先在 App 外部定義變量 i。
leti=0;exportdefaultfunctionApp(){//...}接着重點來看看 add 方法。
調用 add,會向 list 中添加新的 button,新 button 也綁定了 onClick={add}。
constadd=()=>{setList(list.concat(<buttonkey={i}onClick={add}>{i++}</button>));};當我們點擊「Add 按鈕」7 次,會展示:

在線示例:https://codesandbox.io/s/awesome-edison-hrfcku?file=/src/App.js
問題現在問題來了:現在我們點擊這些「數字按鈕」,頁面會怎麼展示呢
你可以先停下來思考一下,再繼續往下讀。
解答有的同學可能會認為,點擊「數字按鈕」後,會有新的 button 被添加到 list 中。
先說結論,這個答案並不正確。
真正的現象是,點擊數字按鈕後:
文字不太容易理解,舉一個🌰。
假設當前列表為:

我們點擊 0:

如果點擊 2:
為什麼會這樣呢?
造成這種反直覺現象的原因有兩個:
再來看看點擊按鈕會調用的 add 函數:
constadd=()=>{setList(list.concat(<buttonkey={i}onClick={add}>{i++}</button>));};當執行 add 函數時,由於訪問了外層函數 App 內的變量,所以會根據 App 函數上下文形成閉包,閉包內包括:
list 和 setList 是調用 useState() 返回的。
這裡通常有一個誤解:多次調用 useState,返回的 list 都是同一個對象。
實際上,useState 返回的 list 都是基於 base state 計算出來的:
currentstate=basestate+update1+update2+…每次會將上一次的 prev state 與 update 進行合併得到新的 current state。
因此,每次調用 useState 返回的 list 都不是同一個對象,它們的內存地址不同。
這會導致每個「數字按鈕」的 add 函數處於不同的閉包中,每個閉包當中的 list 都不同。

而變量 i 是聲明在 App 外層的模塊級變量,在每個閉包中 i 都是相同的。
leti=0;exportdefaultfunctionApp(){//...}所以,在點擊 0 時:
add 函數實際上執行的是:
setList([].concat(<buttonkey={7}onClick={add}>{7}</button>));所以 list 最終變成了 [7]。
當點擊 2 時:
add 函數實際上執行的是:
setList([0,1].concat(<buttonkey={7}onClick={add}>{7}</button>));所以 list 最終變成了 [0, 1, 7]。
為了方便理解,這裡的 [0, 1, 7] 省略了外層的 <button> 標籤
如何解決那麼如何解決這個閉包問題,在 list 後面正常拼接 button 呢?
很簡單,只要將 list 從閉包中清理出去就可以了,將 setList 參數改為函數形式。
之前是:
setList(list.concat(<buttonkey={i}onClick={add}>{i++}</button>));修改為:
setList(list=>list.concat(<buttonkey={i}onClick={add}>{i++}</button>));這樣,我們點擊「Add 按鈕」或任意「數字按鈕」都會正常在 list 後面拼接新按鈕。
大家可以通過在線示例來加深理解。
在線示例:https://codesandbox.io/s/awesome-edison-hrfcku?file=/src/App.js
總結由於 state 的更新機制是:
currentstate=basestate+update1+update2+…所以每次調用 useState,返回值 list 都是不同的對象。
並且由於閉包的存在,每個「數字按鈕」add 函數中的 list 都不同。
兩者共同作用,造成了這種不符合直覺的現象。
如何解決這種閉包問題呢?我們可以將 setState 改為函數形式,將變量從閉包中清理出去。
參考如果覺得有用,就點讚、在看、分享吧,謝謝大家啦~