
當了二十多天廢物,今天開始更新,讓大家久等了。花了挺長的時間優化文章 UI 細節,進一步提高閱讀體驗,大家多多感受一下.
useEffect是一個難以掌握的知識點。許多人對它半知半解,因此他們覺得函數式組件不受控制。
除了本身難以理解之外,React 還提供了一個類似的 hook:useLayoutEffect來增加學習難度,對於新手來說,這可要了老命了。
許多朋友試圖利用 class 語法中的生命周期來類比理解 useEffect,因為官方文檔就是這麼引導的,那麼他們多半會陷入一些誤區,因此,學習之前,大家需要明確的是,生命周期函數與 useEffect 是不同的。
要充分理解並使用該方法,你需要對閉包、同步、異步、事件循環等基礎概念有清晰認知。
01概念
useEffect可以讓使用者在函數組件中執行副作用操作。
那什麼是副作用操作呢?
在 React 中,由 state 的變化導致 UI 發生變化的過程是正常操作,其他操作行為:例如數據請求、直接手動修改 DOM 節點、直接操作頁面「修改頁面標題等」、記錄日誌等都是副作用操作。
副作用操作是相對於操作 state 而言的。
每一次因為 state 的改變,都有一次對應副作用函數的執行時機。如果 state 多次改變,那麼就有多次對應副作用的執行時機。

例如:我希望記錄點擊的次數。
該次數不僅要在頁面上顯示,也要在頁面標題中顯示。
我們就可以給出如下代碼來實現需求
import{useState,useEffect}from'react';functionExample(){const[count,setCount]=useState(0);useEffect(()=>{//UpdatethedocumenttitleusingthebrowserAPIdocument.title=`Youclicked${count}times`;});return(<div><p>Youclicked{count}times</p><buttononClick={()=>setCount(count+1)}>Clickme</button></div>);}在該例子中,修改頁面標題的行為是副作用行為,因此我們可以直接使用useEffect來定義它。useEffect 的第一個參數為一個回調函數,該回調函數就是我們上面說的副作用函數「effect」,我們想要執行的副作用邏輯都寫在該函數中。

語法
//中括號表示參數可選useEffect(effct[,deps])useEffect是 React 提供的 Hook,它能夠幫助我們定義 effect 函數。
第一個參數就是副作用函數 effect
第二個參數表示依賴項,是一個可選參數。當不傳入該參數時,每次 UI 渲染 effect 函數都會執行。
但是大多數時候我們並不想任何 state 的變化都一定要執行 effect 函數,這個時候我們可以傳入依賴項數組。使用時請確保依賴項數組中為 state/props 的值,表示 effect 只會響應依賴項中狀態的變化。
如果你在 useEffect 中傳入與 state 無關的數據,effect 不會響應它們
只有當依賴項中是 state 發生變化時,effect 才會與之對應的執行
不同的 state 數據變化通常對應不同的副作用操作。因此我們可以在函數組件中,定義多個 effect。
functionDemo(){const[count,setCount]=useState(0)const[show,setShow]=useState(false)useEffect(()=>{//dosomething},[count])useEffect(()=>{//doothersomething},[show])...}除此之外,我們還可以傳入空數組作為依賴項,用於表示依賴項不會發生變化。因此,空數組對應的 effect,就只會在初始化時執行一次,以後就再也不會執行了。
我們通常利用這個特性完成一些初始化工作,例如請求頁面數據
const[list,setList]=useState(0);//DOM渲染完成之後effect函數執行useEffect(()=>{recordListApi().then(res=>{setList(res.data);})},[]);03清除副作用
有的時候,副作用函數 effect 執行會留下一些痕跡,因此 useEffect 提供了一種清除副作用的方式。
effect 與 clear effect 是一一對應的緊密關係。因此,我們可以定義一個回調函數由 effect 執行時返回,該函數就是 clear effect 函數。
useEffect(()=>{//dosomething//定義cleareffect函數return()=>{//clearsomething}},[])這裡一定要注意該函數與 class 組件中的 componentWillUnmount 的區別,官方文檔中的案例存在一定的誤導性。如果 deps 傳入空數據,則兩者是類似的,否則他們完全不一樣,effect 與 clear effect 都有可能執行多次
clear effect 在下次 effect 執行之前執行,也會在組件銷毀之前執行一次。
我們可以藉助該特性實現一個防抖的案例
例如我們要實現一個搜索框的功能。文字輸入過程中會自動發起搜索請求。為了防止請求發送過於頻繁,在高頻輸入時,不發送接口請求,如果超過了 500ms 下一次輸入事件還沒有發生,那麼就自動請求一次。
實現代碼如下
import{useEffect,useState}from'react'exportdefaultfunctionEffectDemo(){const[text,setText]=useState('')useEffect(()=>{lettimer=setTimeout(()=>{console.log('發送搜索請求')},500)return()=>{console.log('清除定時器')clearTimeout(timer)}},[text])return(<div><inputtype="text"placeholder='請輸入內容...'onChange={(e)=>setText(e.target.value)}/></div>)}我們在 effect 中定義了定時器,作為延遲操作:500ms 後執行請求邏輯。如果下一次 text 快速發生變化,clear effect 執行會清除掉上一次定義的定時器任務,那麼請求邏輯就不會執行。
只有下一次 text 的改變超過了 500ms 時,定時器任務才會如期執行。
執行順序為

案例
在學習和理解 effect 的含義時,我們知道 state 的變化引發 UI 重新渲染,UI 渲染完成之後會執行 effect。
然而在真實實踐時,我們往往是知道自己要執行的副作用邏輯是什麼,難的是需要我們自己去設計合理的 state。不合理的設計會讓程序變得複雜。
現在我們要來實現下面的動畫效果:

一、點擊紅色畫布,白色方塊執行第一段動畫,並顯示執行日誌
二、執行完後緊接着執行第二段動畫回到圓點,並顯示執行日誌
三、在白色方塊執行動畫的過程中點擊事件無效:點擊不影響動畫的執行,結束之後重新生效
這個效果的實現,最重要的是對於幾個狀態的設計
首先,我們需要用一個狀態來表示第一段動畫的執行與否anime01
其次,我們需要用一個狀態來表示第二段動畫的執行與否anime02
最後,我們也可以使用一個額外的狀態來判斷整個過程是否已經執行完畢 stoped。
重點思考該狀態的特性,與存在的必要性
在實現該邏輯中,我們只需要知道每一個運動的結束時間點,並修改對應的狀態即可。
例如:第一段動畫執行結束,修改 anime02 為true.
完整代碼如下:
import{useState,useRef,useEffect}from'react';//@ts-ignoreimportanimefrom'animejs';import'./style.scss';exportdefaultfunctionAnimateDemo(){const[anime01,setAnime01]=useState(false);const[anime02,setAnime02]=useState(false);constelement=useRef<HTMLDivElement>(null);//是否已經停下來了conststoped=useRef(true)useEffect(()=>{anime01&&animate01();anime02&&animate02();},[anime01,anime02]);functionanimate01(){anime({targets:element.current,translateX:400,backgroundColor:'#FF8F42',borderRadius:['0%','50%'],complete:()=>{setAnime01(false)setAnime02(true)}})}functionanimate02(){anime({targets:element.current,translateX:0,backgroundColor:'#FFF',borderRadius:['50%','0%'],easing:'easeInOutQuad',complete:()=>{setAnime02(false);stoped.current=true}})}functionclickHandler(){if(stoped.current){stoped.current=falsesetAnime01(true);}}return(<divclassName="container"onClick={clickHandler}><divclassName="el"ref={element}/>{anime01&&<div>第一段動畫執行中</div>}{anime02&&<div>第二段動畫執行中</div>}</div>)}這個案例值得我們進一步思考,一方面是數據為什麼需要使用 state 或者 ref. 另一方面是關於 effect 是否還有另外一個角度的思考。我們下一章接着分享.
05推薦閱讀
React 知命境

