React18的useEffect執行兩次如何應對
前段時間在本地啟了一個 React Demo 項目,在編碼的過程中遇到一個很奇怪的“Bug”。
其中簡化版的代碼如下所示。
// 入口文件import { StrictMode } from 'react';import * as ReactDOMClient from 'react-dom/client';import App from './App';const root = ReactDOMClient.createRoot(document.getElementById('root'));root.render( <StrictMode> <App /> </StrictMode>);// 組件代碼import React, { useEffect } from 'react';const App = () => { useEffect(() => { console.log('組件掛載完成!'); }, []); return <>Hello world!</>;};我是萬萬沒想到,就這樣幾行簡單的代碼竟然會觸發一個“Bug”。
此“Bug”的表現為:在 Chrome 控制臺里發現 “Hello world!” 被打印了 “兩次”。
刷新之后依然如此,當時就給我整懵了,第一感覺就是,這怎么可能?
很是糾結一番之后依然沒想明白,于是試著去網上搜了一下,發現竟然有人同樣遇到過這個問題。
通過網上指引,同時去官網查了一下,終于得出答案。
這不是 Bug,這是 React18 新加的特性。
二、React18 useEffect 新特性1.這是 React18 才新增的特性。2.僅在開發模式("development")下,且使用了嚴格模式("Strict Mode")下會觸發。 生產環境("production")模式下和原來一樣,僅執行一次。3.之所以執行兩次,是為了模擬立即卸載組件和重新掛載組件。 為了幫助開發者提前發現重復掛載造成的 Bug 的代碼。 同時,也是為了以后 React的新功能做鋪墊。 未來會給 React 增加一個特性,允許 React 在保留狀態的同時,能夠做到僅僅對UI部分的添加和刪除。 讓開發者能夠提前習慣和適應,做到組件的卸載和重新掛載之后, 重復執行 useEffect的時候不會影響應用正常運行。
如何應對看過文檔以及了解他們這么做的本意之后,我也能夠理解他們會這樣做了。
只是,對于這種半強迫式操作多少有些不喜歡,感覺是在代碼中”被強迫打一針疫苗?”。
當然,人家就是這么干了,作為 React 的普通使用者,能做的就是 適應它 ,并按照它的規范來做。
1.首先先了解一下 React 中 useEffect 執行的時機Every time your component renders, React will update the screen and then run thecode inside useEffect.
每次組件渲染時,React 都會更新頁面 UI,然后運行 useEffect 中的代碼。
Effects run at the end of the rendering process after the screen updates
Effect 在屏幕更新之后的 rendering 進程結束的時候執行。
從上面可以得出結論,React 中的 useEffect 執行時機是在組件渲染之后(類似于 window(component).onload ?)。
因此,對于某些“副作用”的渲染,比如異步接口請求,事件綁定等操作我們通常都放在 useEffect 中執行。
當然,useEffect 除了在組件渲染的時候執行外,在組件卸載的時候也有相關執行操作。
在組件卸載的時候會執行 useEffect 方法的return語句。
useEffect(() => { window.a = 100; return (window.a = 0);}, []);如上代碼段,當組件渲染的時候會執行window.a = 100,當組件卸載的時候會執行window.a = 0。
知道了 useEffect 的執行時機,也就能明白為什么 React18 中 useEffect 會執行兩次了。
因為, React18 在開發環境中除了必要的掛載之外,還 "額外"模擬執行了一次組件的卸載和掛載。
既然知道了原因,那么,接下來就是想辦法解決了。
2.怎么樣才能讓 Effect 執行一次?。對于這個問題,官方文檔上面有一句原話:The right question isn’t “how to run an Effect once,” but “how to fix my Effect so that it works after remounting”.翻譯一下,就是說:正確的問題不是“怎么樣讓 Effect 執行一次”,而是“怎樣修復我的 Effect,讓它在(重復)掛載之后正常工作”
也可以理解,畢竟在 React 的未來版本中做離屏渲染的時候 useEffect 肯定會多次執行的。
而且,即使是當前版本,在做頁面的前進后退也會面臨觸發多次 useEffect。
所以,解決辦法其實就是解決 重復掛載卸載之后 應用正常工作了。
###3.具體的解決方法我們知道 useEffect 支持返回一個函數,在組件卸載的時候就會執行該函數。
因此,通常正確解法就是 實現清理函數,并將其在 useEffect 中返回。
當然,不同的 Effect 需要有不同的清理方式。
在常用 Effect 分類下,大致有如下幾類清理。
1)清理事件監聽
useEffect(() => { function handleScroll(e) { console.log(e.clientX, e.clientY); } window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll);}, []);對于事件監聽類函數,在返回函數內部“取消掉事件監聽”即可。
2-1)重置頁面數據,清理屬性狀態
useEffect(() => { const node = ref.current; node.style.opacity = 1; // Trigger the animation return () => { node.style.opacity = 0; // Reset to the initial value };}, []);對于一些頁面屬性的變更,在返回函數內部將其變更的屬性進行還原。
2-2)重置頁面數據,還原元素狀態
import { useEffect, useRef } from 'react';function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } }); return <video ref={ref} src={src} loop playsInline />;}涉及到元素狀態的,比如播放器之類,需要對(元素)播放器的狀態進行重置。
2-3)重置頁面數據,彈窗類。
useEffect(() => { const dialog = dialogRef.current; dialog.showModal(); return () => dialog.close();}, []);如果是默認彈窗類,這種也算是元素狀態,同樣需要對其(彈出)狀態進行重置。
3-1)異步請求頁面數據處理,處理異步數據渲染
useEffect(() => { let ignore = false; async function startFetching() { const json = await fetchTodos(userId); // 這里執行是異步的,所以第一次執行到此處的時候組件已經被卸載了 // 此時的 ignore 已經被 return 里面的方法置為 true 了 // 所以這里第一次執行的時候不執行 setTodos(json) // setTodos 其實是在第二次執行的時候才觸發 if (!ignore) { setTodos(json); } } startFetching(); return () => { ignore = true; };}, [userId]);如上代碼,對于異步請求數據并渲染這一類。
我們可以設置一個 標識位,做到對 請求返回的數據 僅做一次處理與渲染setTodos(json)。
codesandbox 測試代碼段
3-2)異步請求頁面數據處理,處理接口請求
上面的方法雖然僅會渲染一次,但是請求依然發起了多次。
如果不希望請求多次,也可以使用請求接口數據的緩存方案,對返回數據進行緩存。
const cache = useRef(null);useEffect(() => { let ignore = false; async function startFetching() { if (!cache.current) { cache.current = await fetchTodos(userId); } if (!ignore) { setTodos(cache.current); } } startFetching(); return () => { ignore = true; };}, [userId]);對于異步請求,除了可以處理渲染頻率,還可以對接口的請求本身做緩存。
在前面3-1的基礎上,緩存接口返回的數據,下次請求的時候如果已經有緩存數據了就直接用,無須再次發起請求。
4)無須清理類
并不是所有的 useEffect 函數都需要清理,對于一些沒有副作用的函數,我們完全可以不做處理
useEffect(() => { const map = mapRef.current; map.setZoomLevel(zoomLevel);}, [zoomLevel]);如上代碼所示,setZoomLevel 方法僅僅是設置一下 Dom 元素的層級。這種操作無論同時執行多少次都不會有太大的影響,所以對于這一類我們就隨他去吧,畢竟線上也不會執行多次。
5)日志 log 上報類
useEffect(() => { reportLog({ name: 'viewCount' });}, []);對于日志上報類,其實也可以算是無須清理類,但是又有點特殊。
因為,對于日志類,首先在開發環境中我們其實是無須進行上報的,畢竟這種日志打上去也沒啥用。
當然,如果是要對上報日志本身這個進行調試等必須上報的情形,這種也有三種應對方式:
方式一,在本地開發環境使用 console.log 來代替 reportLog。方式二,取消掉嚴格模式(StrictMode) 方式三,構建一個 production版本啟動,或者將其部署到 QA 環境,部署的時候,指定 production 模式。
借鑒鏈接:大神地址:epoos
總結到此這篇關于React18的useEffect執行兩次該如何應對的文章就介紹到這了,更多相關React18 useEffect執行兩次內容請搜索好吧啦網以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持好吧啦網!