網站首頁 編程語言 正文
這篇文章會假設你對useEffectAPI有一定程度的了解。
一、每一次渲染都有它自己的 Props and State
在我們討論 effects 之前,我們需要先討論一下渲染,當我們更新 state 的時候,React會重新渲染組件。每一次渲染都能拿到獨立的 state,這個狀態值是函數中的一個常量。
這里關鍵的點在于任意一次渲染中的常量都不會隨著時間改變。渲染輸出會變是因為我們的組件被一次次調用,而每一次調用引起的渲染中,它包含的值獨立于其他渲染。
如果 props 和 state 在不同的渲染中是相互獨立的,那么使用到它們的任何值也是獨立的(包括事件處理函數)。它們都“屬于”一次特定的渲染。即便是事件處理中的異步函數調用“看到”的也是這次渲染中的值。
二、每次渲染都有它自己的Effects
讓我們先看向官網的 useEffect 的例子:
function Counter() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
effect是如何讀取到最新的count
狀態值的呢?
也許,是某種 watching 機制類似 vue 中的數據響應式使得能夠在 effect 函數內更新?也或許是一個可變的值,React 會在我們組件內部修改它以使我們的 effect 函數總能拿到最新的值?
都不是。
我們已經知道是某個特定渲染中的常量。事件處理函數“看到”的是屬于它那次特定渲染中的狀態值。對于 effects 也同樣如此:
并不是count
的值在“不變”的 effect 中發生了改變,而是 effect 函數本身在每一次渲染中都不相同。
React 會記住你提供的 effect 函數,并且會在每次更改作用于DOM并讓瀏覽器繪制屏幕后去調用它。
所以雖然我們說的是一個 effect(這里指更新document的title),但其實每次渲染都是一個不同的函數— 并且每個 effect 函數看到的 props 和 state 都來自于它屬于的那次特定渲染。
三、關于依賴項不要對React撒謊
現在只需要記住:如果你設置了依賴項,effect 中用到的所有組件內的值都要包含在依賴中。這包括props,state,函數組件內的任何東西。
在下面這個組件中,我們的直覺是:“開啟一次定時器,清除也是一次”。直覺上我們會設置依賴為 '[]'。“我只想運行一次 effect ”,但是這樣對嗎?
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, []); return <h1>{count}</h1>; }
我們以為他會一直遞增下去,但實際上他只會遞增一次,你想要觸發一次因為它是定時器 ,但為什么會有問題?
在第一次渲染中我們執行了setCount(0 + 1)
。但是我們設置了[]
依賴,effect不會再重新運行,它后面每一秒都會調用setCount(0 + 1)。
我們對 React 撒謊說我們的 effect 不依賴組件內的任何值,可實際上我們的 effect 有依賴。
四、兩種誠實告知依賴的方法
第一種策略是在依賴中包含所有 effect 中用到的組件內的值。讓我們在依賴中包含:count
useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, [count]);
這在我們大部分初級開發者的眼中都沒有什么問題,并且程序確實不會出任何 bug,現在,每次修改都會重新運行 effect,這能解決問題但是我們的定時器會在每一次改變后清除和重新設定。這肯定不是我們想要的結果。
第二種策略是修改 effect 內部的代碼以確保它包含的值只會在需要的時候發生變更。
在這個場景中,我們其實并不需要在effect中使用 count。當我們想要根據前一個狀態更新狀態的時候,我們可以使用的函數形式:
useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id); }, []);
我們需要告知React的僅僅是去遞增狀態 - 不管它現在具體是什么值。注意我們做到了移除依賴,并且沒有撒謊。我們的 effect 不再讀取渲染中的count
值。
五、來自useReducer的助攻
如果我們有兩個互相依賴的狀態,或者我們想基于一個 prop 來計算下一次的 state,setCount(c => c + 1)
它并不能做到。幸運的是,有一個更強大的姐妹模式,它的名字叫useReducer。
我們先來修改上面的例子讓它包含兩個狀態:count 和 step 。我們的定時器會每次在 count 上增加一個 step 值:
function Counter() { const [count, setCount] = useState(0); const [step, setStep] = useState(1); useEffect(() => { const id = setInterval(() => { setCount(c => c + step); }, 1000); return () => clearInterval(id); }, [step]); return ( <> <h1>{count}</h1> <input value={step} onChange={e => setStep(Number(e.target.value))} /> </> ); }
注意我們沒有撒謊。既然我們在 effect 里使用了step
,我們就把它加到依賴里。所以這也是為什么代碼能運行正確。
這個例子目前的行為是修改會重啟定時器 - 因為它是依賴項之一。在大多數場景下,這正是你所需要的。清除上一次的effect然后重新運行新的effect并沒有任何錯。不過,假如我們不想在改變后重啟定時器,我們該如何從effect中移除對的依賴呢?
下面這句話我希望你作為一名 react 開發人員要記下來:
當你想更新一個狀態,并且這個狀態更新依賴于另一個狀態的值時,你可能需要用useReducer
去替換它們。
reducer 可以讓你把組件內發生了什么(actions)和狀態如何響應并更新分開表述。
我們用一個dispatch
依賴去替換 effect 的依賴 step
:
function Counter() { const [state, dispatch] = useReducer(reducer, initialState); const { count, step } = state; useEffect(() => { const id = setInterval(() => { dispatch({ type: 'tick' }); }, 1000); return () => clearInterval(id); }, [dispatch]); return ( <> <h1>{count}</h1> <input value={step} onChange={e => { dispatch({ type: 'step', step: Number(e.target.value) }); }} /> </> ); } const initialState = { count: 0, step: 1, }; function reducer(state, action) { const { count, step } = state; if (action.type === 'tick') { return { count: count + step, step }; } else if (action.type === 'step') { return { count, step: action.step }; } else { throw new Error(); } }
你可能會問:“這怎么就更好了?”答案是React會保證dispatch
在組件的聲明周期內保持不變。所以上面例子中不再需要重新訂閱定時器。
相比于直接在 effect 里面讀取狀態,它 dispatch 了一個action來描述發生了什么。這使得我們的 effect 和狀態解耦。我們的 effect 不再關心怎么更新狀態,它只負責告訴我們發生了什么。更新的邏輯全都交由 reducer 去統一處理。
六、把函數移到Effects里
一個典型的誤解是認為函數不應該成為依賴。舉個例子,下面的代碼看上去可以運行正常:
function SearchResults() { const [data, setData] = useState({ hits: [] }); async function fetchData() { const result = await axios( 'https://hn.algolia.com/api/v1/search?query=react', ); setData(result.data); } useEffect(() => { fetchData(); }, []); // ...
需要明確的是,上面的代碼可以正常工作。但這樣做在組件日漸復雜的迭代過程中我們很難確保它在各種情況下還能正常運行。
如果我們在某些函數內使用了某些 state 或者 prop:
function SearchResults() { const [query, setQuery] = useState('react'); // Imagine this function is also long function getFetchUrl() { return 'https://hn.algolia.com/api/v1/search?query=' + query; } // Imagine this function is also long async function fetchData() { const result = await axios(getFetchUrl()); setData(result.data); } useEffect(() => { fetchData(); }, []); // ... }
如果我們忘記去更新使用這些函數(很可能通過其他函數調用)的effects的依賴,我們的effects就不會同步props和state帶來的變更。這當然不是我們想要的。
如果某些函數僅在effect中調用,你可以把它們的定義移到effect中:
function SearchResults() { // ... useEffect(() => { // We moved these functions inside! function getFetchUrl() { return 'https://hn.algolia.com/api/v1/search?query=react'; } async function fetchData() { const result = await axios(getFetchUrl()); setData(result.data); } fetchData(); }, []); }
這么做有什么好處呢?我們不再需要去考慮這些“間接依賴”。我們的依賴數組也不再撒謊:在我們的 effect 中確實沒有再使用組件范圍內的任何東西。
如果我們后面修改getFetchUrl
去使用狀態 query
,我們更可能會意識到我們正在effect里面編輯它因此,我們需要把 query
添加到effect的依賴里:
function SearchResults() { const [query, setQuery] = useState('react'); useEffect(() => { function getFetchUrl() { return 'https://hn.algolia.com/api/v1/search?query=' + query; } async function fetchData() { const result = await axios(getFetchUrl()); setData(result.data); } fetchData(); }, [query]); }
七、我不想把可復用的函數放到Effect里
有時候你可能不想把函數移入 effect 里。比如,組件內有幾個 effect 使用了相同的函數,你不想在每個 effect 里復制黏貼一遍這個邏輯。也或許這個函數是一個 prop。
在這種情況下你應該忽略對函數的依賴嗎?這么做是不對的。再次強調,effects不應該對它的依賴撒謊。通常我們還有更好的解決辦法。一個常見的誤解是,“函數從來不會改變”。但是這篇文章你讀到現在,你知道這顯然不是事實。實際上,在組件內定義的函數每一次渲染都在變。
function SearchResults() { function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query; } useEffect(() => { const url = getFetchUrl('react'); // ... Fetch data and do something ... }, []); useEffect(() => { const url = getFetchUrl('redux'); // ... Fetch data and do something ... }, []); }
在這個例子中,你可能不想把getFetchUrl
移到 effects 中,因為你想復用邏輯。
另一方面,如果你對依賴很“誠實”,你可能會掉到陷阱里。我們的兩個 effects 都依賴 getFetchUrl
,而它每次渲染都不同,所以我們的依賴數組會變得無用:
function SearchResults() { function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query; } useEffect(() => { const url = getFetchUrl('react'); // ... Fetch data and do something ... }, [getFetchUrl]); useEffect(() => { const url = getFetchUrl('redux'); // ... Fetch data and do something ... }, [getFetchUrl]); // ... }
我們有兩個更簡單的解決辦法。
第一個, 如果一個函數沒有使用組件內的任何值,你應該把它提到組件外面去定義,然后就可以自由地在 effects 中使用:
function getFetchUrl(query) { return 'https://hn.algolia.com/api/v1/search?query=' + query; } function SearchResults() { useEffect(() => { const url = getFetchUrl('react'); // ... Fetch data and do something ... }, []); useEffect(() => { const url = getFetchUrl('redux'); // ... Fetch data and do something ... }, []); // ... }
你不再需要把它設為依賴,因為它們不在渲染范圍內,因此不會被數據流影響。
或者, 你也可以把它包裝成useCallback Hook:
function SearchResults() { const getFetchUrl = useCallback((query) => { return 'https://hn.algolia.com/api/v1/search?query=' + query; }, []); useEffect(() => { const url = getFetchUrl('react'); // ... Fetch data and do something ... }, [getFetchUrl]); useEffect(() => { const url = getFetchUrl('redux'); // ... Fetch data and do something ... }, [getFetchUrl]); // ... }
我們用 useCallback 對 getFetchUrl 做了一層緩存,現在只有當依賴項變化的時候,才會重新執行 useCallback 來返回新的函數,依賴項沒有變化的時候就算組件 rerender 了,這個函數也不會重新執行,這樣我們把 getFetchUrl 作為 useEffect 的依賴就沒問題了。
不同于傳遞參數的方式,現在我們從狀態中讀取 query:
function SearchResults() { const [query, setQuery] = useState('react'); const getFetchUrl = useCallback(() => { return 'https://hn.algolia.com/api/v1/search?query=' + query; }, [query]); useEffect(() => { const url = getFetchUrl(); // ... Fetch data and do something ... }, [getFetchUrl]); // ... }
如果query
保持不變,useCallback
也會保持不變,我們的 effect 也不會重新運行。但是如果修改了 query,useCallback 也會隨之改變,因此會重新請求數據。這就像你在Excel里修改了一個單元格的值,另一個使用它的單元格會自動重新計算一樣。
原文鏈接:https://blog.csdn.net/qq_49900295/article/details/127039404
相關推薦
- 2023-01-28 C#實現繪制鼠標的示例代碼_C#教程
- 2022-04-29 C++?Queue隊列類模版實例詳解_C 語言
- 2023-07-14 react實現拖拽功能
- 2022-10-13 windows?server設置FTP域用戶隔離的方法_FTP服務器
- 2023-04-07 C語言解決字符串中插入和刪除某段字符串問題_C 語言
- 2022-03-21 Prometheus容器化部署的實踐方案_docker
- 2023-05-20 openGauss數據庫共享存儲特性概述_數據庫其它
- 2022-05-11 Feign之間的文件傳輸
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支