前言
看過幾個react hooks 的項目,控制台上幾百條警告,大多是語法不規范,react hooks 使用有風險,也有項目直接沒開eslint。當然,這些項目肯定跑起來了,因為react自身或者其他的包,在編譯的時候彌補了一些缺陷,還有一些是不規范的警告,或者還沒運行到報錯的代碼。
在這,我想分享並解析一些react開發過程中,一些很常見的需求,以及正確的用法,至少也得做到控制台沒有任何警告才行。當然,如果大家有更好的方式,也請留言。
接下來我會把這些問題做個匯總,請看目錄。然后以我會以最常見的表格增刪改查界面舉例,配合代碼做個講解。
每個問題都是我初學react hooks的時候,一步步踩過的坑,錯誤案例肯定沒代碼了,正確的自然有,源代碼在我的GitHub上 點這里,看源碼
這篇博客我會一直更新,想到啥寫啥,當做一個使用記錄本用。
目錄
1、useState如何合理的聲明變量(合並優化state)
2、組件加載后該如何發送 http 請求(useEffect)
4、useEffect中用到了navigate、dispatch 或者其他庫變量,導致多次執行怎么處理
1、useState如何合理的聲明變量(這是個模糊的問題,沒有所謂的唯一答案,所以我想通過一個案例分析過程,幫助你找到最科學的答案)
在class時代,一個組件的所有state都在一個對象中。然而函數組件允許你分離聲明,那現在我們是參考以前僅聲明一個大state變量?還是每個變量都單獨聲明呢?
顯然,都不對。應該是一個折中的狀態。具體如何找這個邊界,我們先看下面的圖片,通過一個例子讓你理解。
看圖,這是一個很簡單的場景,當點擊查詢,請求數據,修改頁碼也要請求數據。以前我們通常會聲明2個屬性 search 代表查詢條件,pagination 代表頁碼,接下來按照這個思路試一下。
a、第一次嘗試(失敗了)
看代碼,關於為什么用 useEffect 監聽變量請求數據,我會在 目錄第二條http請求 有詳細說明,我們先看這個例子。
首先,我們先看一下 useState 的用法。 const [變量,update更新函數] = useState(默認值); 這是一個標准的 useState 的用法。然后我們組織下代碼,如下所示,執行一下。功能實現了,看起來沒問題。但是看一下控制台的打印,點擊搜索后,useEffect執行了2次,發了兩次請求,這顯然不符合預期。原因是useState的第二個結果 update 函數是異步執行的,看控制台會發現,pagination變化了,search還沒變化。所以第一次請求只有頁碼,第二次請求才是正確的。顯然這樣寫有問題。以前class組件,我們會在setState的會調中直接請求數據。但是函數組件不行,第一,我並沒有發現update函數有回調。第二,多個update,總不能先更新search,回調里面再更新pagination,回調里面再請求數據。
let [search, setSearch] = useState({}), [pagination, setPagination] = useState({current: 1, pageSize: 3}); // 搜索
const handleSearch = (d) => { setPagination({...pagination, current: 1}); setSearch(d); }; useEffect(() => { const getData = () => {// 這是請求數據的函數};
console.log(search) console.log(pagination) getData(); }, [search, pagination]);
b、第二次嘗試(成功了,沒有任何問題)
分開聲明失敗了,那我們先合並成一個變量,試一下有沒有問題。代碼看下方。運行一下,發現沒問題,控制台只打印一次,請求也只執行一次,非常nice。
結論:
現在我們捋一下這個問題,對於表格來說搜索條件和分頁都是查詢條件,配合使用,而且還存在同時修改的情況。除此之外這兩個變量還分別需要給查詢條件組件和表格頁碼屬性使用。所以數據結構是不能改變的,但是他們的職能對表格來說是一樣的。所以這樣的變量就應該合到一起聲明。分開反而造成很多麻煩。至此,我們明白,至少他倆是需要合並在一起聲明的。
那么有人會問,表格查詢除了這兩個變量,還會有 loading加載狀態,表格數據,表頭、數據總條數等等數據,要不要合並。還是那句話,分析它的功能和使用場景。我們做個變量變化的簡單對比:
請求數據之前:loading、查詢條件、頁碼 可能變化
請求數據之后:loading、表格數據、數據總數total 可能變化
很顯然,根據變化的時機就可以分為3種,如果強行把數據都湊在一起,一方面useEffect無法精細監聽,另一方面,修改一個數據,需要解構所有數據。最合適的方案就是分3次useState聲明。以小觀大,其他變量也都是這樣去分析的。以前class組件無腦定義,現在需要仔細分析每個變量。
let [search, setSearch] = useState({data: {}, pagination: {current: 1, pageSize: 3}}); // 搜索
const handleSearch = (d) => { setSearch({ data: {...d}, pagination: {...search.pagination, current: 1} }); }; useEffect(() => { const getData = () => {查詢數據}; console.log('---------useEffect----2-----') console.log(search) getData(); }, [search]);
2、組件加載后該如何發送 http 請求
相信很多人被react文檔給坑過,看到文檔 useEffect 的第二個參數傳空數組,就只會觸發一次,可以在這里發送http請求獲取數據。哈哈哈!我一開始也被坑了,下邊的代碼和圖片,就是錯誤的寫法和控制台警告。但是,react源碼其實是可以避免這種風險的,只是eslint不知道,這就導致寫法不好,但是功能正常。所以后來react FAQ特地解釋了一下,看下圖。
// 查詢表格數據
const getData = () => {}; useEffect(() => { getData(); }, []);
好吧,錯誤用法已經看過了,那究竟要怎么寫才算正確的呢?現在我們已經可以確定,可以利用useEffect實現http請求,當然別的hooks也能實現。那么在講解正確做法之前,得先看一段代碼,你需要先理解 useEffect 到底是怎么執行的,都在什么時間執行。ps:接下來很關鍵,能讓你徹底理解useEffect,特別是我用紅色字體標記的3個地方。
看代碼,刷新界面,控制台的打印順序應該是 1 2 3。刷新界面之后,函數組件執行,1打印了,然后執行return,渲染頁面,再然后依次執行useEffect,所以 2 3 依次打印。所以useEffect會在dom渲染之后執行,而且是初始化的時候就會執行一次,這個時機就是 componentDidMount。然后,如果現在修改一下變量 c 的值,再看控制台,輸出為 1 2,而且1的地方打印c的值是最新的值,也就說useEffect只會在監聽的變量變化的時候,等dom渲染完了,再次觸發。每次變化都是這樣,這個時機就是 componentDidUpdate 。變量a 、b因為沒有變化,自然就不執行。最后,如果你有其他頁面,換到別的頁面,再看控制台打印 2,只有2。如果你多放幾個console,你會發現useEffect沒有執行,函數組件也沒有執行,只有getData這個方法執行了。因為我寫了一個return getData; 什么意思呢,useEffect里面寫return,就會在組件卸載之前,執行你return的函數,常用於卸載一些監聽或者定時器等等。這個時機就是 componentWillUnmount 。
useEffect(() => {
}, [c]);
useEffect(() => { console.log('--------3---------', a, b) }, [a, b]);
console.log('-----1------',c);
return (<span>111</span>)
大家看文檔都知道class組件那么多生命周期,函數組件只有hooks,然后就不知道該怎么組織代碼邏輯了。又或者知道useEffect能實現上述3個生命周期的功能,卻不知道具體是怎么實現的。現在大家應該都知道useEffect怎么用了吧。接下來我們發http請求就簡單多了。
看代碼,因為useEffect內部是一個閉包,內部使用的變量都必須顯式的聲明依賴,要不然就會報警告,缺少xxx依賴。所以我們直接將請求表格數據的方法聲明在useEffect內部,然后調用,那他使用的所有的props或者state,都應該是他的依賴,需要寫到中括號里面,否則都會報警告。首先代碼這樣寫是沒問題的,也是官方推薦的寫法。這里先把正統確立了,然后打假。
如果大家百度一下,網上可能還會有2種其他做法,useCallback 和 /* eslint-disable */。我將官方的回答截圖放到下邊,大家可以好好看看。推薦的方法就不多說什么了,useCallback 是在萬不得已,實在沒辦法的時候才會使用。而第二種就更搞笑了,直接屏蔽eslint檢測。當我搜到這種文章的時候,差點氣到岔氣,這是在掩耳盜鈴嗎。。。。(當然那種極其特殊需求的情況下可能確實需要屏蔽這個警告)至於官方說的另外2種方法,因為和請求無關,這里我就不多說了,大家可以在函數組件外聲明一個變量,useEffect也是可以監聽的,可以試一下。
最后,言歸正傳,以前class組件的數據請求通常是靠回調觸發,比如修改什么變量直接請求數據。現在不行,比如下邊的寫法,你需要確定這個請求需要的變量,而且這些變量的變化都是需要觸發數據請求的。比如一個增刪改查的界面,表格數據獲取發生在初始化的時候、頁碼變化和點擊查詢的時候。所以,我們需要確保點擊查詢和修改頁碼一定改變變量,其他任何情況,不能修改變量,因為我們靠監聽變量觸發請求。
useEffect(() => { // 查詢表格數據
const getData = () => { setLoading(true); const { data, pagination } = search; const params = { ...data, current: pagination.current, size: pagination.pageSize } getList(params).then(res => { if (res.code === 200) { let d = res.data; setDataSource(d.records); setTotal(d.total) } setLoading(false); }) .catch(err => { setLoading(false); }); }; getData(); }, [search]);
3、如何獲取dom,並綁定監聽事件(useRef)
這也是我們常見的需求,獲取dom,然后動態 addEventListener 某些事件。實現這個功能我們使用useRef、useEffect兩個hooks。
首先,你需要知道 useRef 的三個特點。 第一,他聲明的變量,將存活於組件的所有生命周期,注意是所有,組件注銷,變量自動銷毀; 第二,他可以存儲任意類型變量,不僅僅是dom和普通對象; 第三,他聲明的變量,數據類型是一個對象,對象上有current屬性,賦值操作都在這個屬性上進行,而且useRef聲明的變量值變化了,不會引起函數組件的重新渲染,他只是一個存儲數據的倉庫,數據修改也是實時的。可以簡單理解為react開辟了一塊地址,專門用來存儲你聲明的變量,后續操作只是不斷往這個地址換數據而已,組件注銷,地址釋放,都不需要我們額外操心。ps:我在想既然變量注銷了,我真的還需要移除監聽嗎,這是我的疑問,不過我沒法去印證這個事情。。。
上面是useRef的特點,然后我們看代碼,節約篇幅,我刪掉了很多,源碼在GitHub src\components\c-large-select 這個文件。其實這個功能簡單,useRef聲明變量,然后綁定到div標簽的ref屬性上,這樣dom渲染之后 scrollEle.current 就可以拿到dom了,然后再useEffect中添加事件綁定,不懂的,看目錄第二條,我詳細介紹了useEffect的執行時機。這里要注意一點,就是根據你的業務邏輯監聽依賴,不要頻繁的去做事件綁定。然后就是useEffect的return函數,執行刪除監聽的操作。
useRef的用法很多,這里僅介紹如何實現我們常用的獲取dom,綁定事件的功能。
const scrollEle = useRef(); // 滾動條dom對象
useEffect(() => { const handleMouse = (v) => {}; const handleScroll = (e) => {}; // 初始化事件
const init = () => { if(list.length > rows) { scrollEle.current.scrollTop = 0 scrollEle.current.addEventListener('scroll', handleScroll); scrollEle.current.addEventListener('mousedown', () => handleMouse(true)); scrollEle.current.addEventListener('mouseup', () => handleMouse(false)); } } // 卸載前,取消監聽
const unInit = () => { if(list.length > rows && scrollEle.current) { scrollEle.current.removeEventListener('scroll', handleScroll, false); scrollEle.current.removeEventListener('mousedown', handleMouse, false); scrollEle.current.removeEventListener('mouseup', handleMouse, false); } }; init(); return unInit; }, [list, rows, rowHeight, listHeight]);
return (<div ref={scrollEle}></div>)
4、useEffect中用到了navigate、dispatch 或者其他庫變量,導致多次執行怎么處理
在目錄的第2條我們已經聊過useEffect執行機制,及發http請求的問題了,關於用法我不贅言。那么有些情況下我們會在useEffect中用到navigate、dispatch 甚至是其他一些庫文件,比如echarts等等,如果用到了,eslint提示我們需要注入依賴。這個時候又該怎么辦。這里有一個感覺不算特別合適辦法,用 useRef 處理。
看下邊的刪減代碼(源碼文件 src\layouts\BasicLayout.jsx),具體場景是我請求數據,校驗權限,存儲redux,沒權限的跳轉登陸。如果useEffect直接監聽navigate,那每次跳轉路由,navigate都會改變,導致useEffect執行,這不是我想要的。所以我們用useRef取一下navigate,useEffect中使用這個變量。因為useRef聲明的變量不會引起函數組件的變化,useEffect自然監聽不到。這樣可以變相的規避這個警告。包括其他場景也是,比如 a=b+c,但是你只想在b變化的時候重新計算,c只需要獲取他最新的值即可,那用useRef是最合適的方式。
const navigate = useNavigate(); const navigation = useRef(navigate); useEffect(() => { dispatch(setUser(param)) navigation.current.navigate('/login'); }, [dispatch, navigation]);
5、useMemo 什么時候用?怎么用?
useMemo他是一個輔助hook,官方建議大量使用,當然不是亂用,適可而止。既然是輔助鈎子,也就是說,不用,你的代碼照常運行,只是可能比較費CPU,嚴重的系統就很卡頓。用了,會提升系統的運行速度和流暢度。
那什么時候用?第一,不用他,代碼邏輯正常,加上他,邏輯也不會變,只是減少了渲染次數; 第二,你很確定某個地方會多執行多次,而且清楚哪些變量變化才應該重新渲染。做到這兩點就不會亂用useMemo了,官方也提示了,絕對不能利用useMemo的特性去實現自己的代碼邏輯,有需要用其他鈎子。
看代碼(文件路徑 src\layouts\BasicLayout.jsx),我們先了解一下 useMemo 的執行機制,看控制台,輸出 1 2 3 ,思考一下,不難發現,useMemo 的執行時機很早,函數組件第一次執行的時候,他就執行了,而且執行在 return 的前面。然后,如果監聽的變量變化,他會再次執行,否則就不會在執行。
再看代碼,我有一個渲染路由的函數,他沒有使用任何變量,所以我認為他這輩子執行一次就夠了,不需要任何依賴再次觸發更新。所以我傳了空數組。當然如果你用了別的依賴,放到數組中監聽就好了。
最后,react官方建議我們大量使用,但實際上我們不必較真,比如一個增刪改查界面,隨便一個變量變化都會重新執行函數組件,我們沒必要將所有組件都用useMemo都包裹一下。那樣代碼可讀性也會降低很多。只是在那些組件嵌套層級比較深,比如3層、4層這種的,如果因為父組件執行會引起子組件不斷執行,就需要useMemo優化一下。比如,像我渲染路由的這種情況。
// 遞歸路由 const mapMenu = (l) => {}; const BasicLayout = () => { console.log('-----1------') // 渲染路由,減少頁面渲染次數 const renderRoute = useMemo(() => { console.log('-----2---') mapMenu(MENU) }, []); console.log('-----3------') return (<div >2121212</div>)
}