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