你覺得Hooks這一點煩嗎?
大家好,我卡頌。
昨天一個小伙伴發了一個Demo給我,讓我解釋下原因。
我一看,好家伙,小小一個Demo,知識點囊括了:
- Hooks的閉包問題
- state是如何組裝的
相信看完這個Demo,對函數組件會有更深的認識。
讓人懵逼的Demo
Demo包含一個按鈕、一個列表。
- <div className="App">
- <button onClick={add}>Add</button>
- {list.map(val => val)}
- </div>
點擊按鈕,調用add方法,向列表中插入一項:
- let i = 0;
- export default function App() {
- const [list, setList] = useState([]);
- const add = () => {
- // ...
- };
- return (
- <div className="App">
- <button onClick={add}>Add</button>
- {list.map(val => val)}
- </div>
- );
- }
顯示效果:
燒腦的地方在于,調用add方法插入的是一個「點擊后會調用 add 方法的按鈕」:
- const add = () => {
- setList(
- list.concat(
- <button
- key={i}
- onClick={add}>
- {i++}
- </button>
- )
- );
- };
點擊Add按鈕7下后的顯示效果:
那么問題來了,點擊帶數字按鈕(會調用和點擊Add按鈕一樣的add方法)后會有什么效果呢?
state的組裝和閉包問題
如果你認為會插入一個新按鈕:
那就錯了。
正確答案是:點擊對應按鈕后list長度變為「按鈕對應數字 + 1」,且最后一項的數字為「點擊前最大數字 + 1」。
比如,點擊前最大數字為6
如果點擊 0,list長度變為0 + 1 = 1,且最后一項為6 + 1 = 7:
如果點擊 2,list長度變為2 + 1 = 3,且最后一項為6 + 1 = 7:
這是兩個因素共同作用的結果:
- Hooks的閉包問題
- state是如何組裝的
原因分析再來看看add方法:
- const add = () => {
- setList(
- list.concat(
- <button
- key={i}
- onClick={add}>
- {i++}
- </button>
- )
- );
- };
button點擊后調用add,所以會基于add所屬上下文(App函數)形成閉包,閉包中包括:
- add
- list
- setList
- i屬于module級作用域,不在該閉包內
其中list與setList來自于useState調用后的返回值:
- const [list, setList] = useState([]);
一種常見的認知誤區是:多次調用useState返回的list是同一個引用。
事實上,每次調用useState返回的list都是基于如下公式計算得出的:
- 基準state + update1 + update2 + ... = 當前state
所以是一個全新的對象。
- 如果你想了解更多update、state計算的細節,參考React技術揭秘[1]
當首屏渲染時:
- App組件首次render
- 創建list = []
- <button onClick={add}>Add</button>依賴add,形成閉包,閉包中的list = []
接下來,點擊Add按鈕:
- 調用add方法,該方法來自于首屏渲染創建的閉包
- add方法中依賴的list來自于同一個閉包,所以list = []
- <button key={i} onClick={add}>{i++}</button>依賴add,形成閉包,閉包中的list = []
所以,對于按鈕0,
任何時候點擊他實際上執行的都是:
- setList(
- [].concat(
- <button key={i} onClick={add}>{i++}</button>
- )
- );
那么如何修復這個問題呢,也很簡單,將setList的參數改為函數形式:
- // 之前
- setList(list.concat(<button key={i} onClick={add}>{i++}</button>));
- // 之后
- setList(list => list.concat(<button key={i} onClick={add}>{i++}</button>));
函數參數中的list來自于Hooks中保存的list,而不是閉包中的list。
總結
由于Hooks總是在組件render時才會計算新狀態,這為Hooks帶來比較重的心智負擔。
相比而言,采用「細粒度更新」實現的Hooks(比如VUE的Composition API)可以實時更新狀態,操作起來更符合直覺。
在使用Hooks過程中,你有沒有遇到類似的頭疼問題呢?
參考資料
[1]React技術揭秘:
https://react.iamkasong.com/state/mental.html#%E5%90%8C%E6%AD%A5%E6%9B%B4%E6%96%B0%E7%9A%84react