本文是在:https://juejin.im/post/5ceb36dd51882530be7b1585 的基礎上進行的探究,非常建議閱讀原文
一、useEffect 依賴誠實問題的粗暴解決及帶來的問題
之前的一個例子,在 useEffect 中直接執行 setInterval 導致依賴欺騙帶來的很多問題,詳細的內容請移步至:
const [count, setCount] = useState(0); const [step, setStep] = useState(1); useEffect(() => { console.log('render useEffect') const id = setInterval(() => { setCount(prevCount => prevCount + step); setStep(step => step + 1); console.log(`[] count is ${count}, step is ${step}`); }, 1000); return () => clearInterval(id); }, [step]);
上面代碼中,雖然通過 setStaet(prevState => prevState + 1)
這樣的方式取消對 count 的依賴,但是一旦代碼里面同時依賴了兩個 state,就無法通過這種方式解決。
上面的代碼中,最終解決的方案其實是在 useEffect 中依賴了 step
,這已經是依賴誠實,但是造成的結果是顯而易見的:每次 step 的變動都會導致重新實例化一次 setInterval 。
二、使用 useReducer 解決依賴誠實問題
我們最終的目的是 useEffect 本身的依賴只有 [ ]
,以為只有 [ ]
我們才能保證組件實例掛載的時候只會執行一次 setInterval
首先我們的依賴關系是發生在 useState
上的(具體的是 setCount),如果能夠解決 setCount 中本身對 count 和 step 的依賴關系是最好的。
而 react 的文檔中,明確提出了 useReducer
是 useState
的替代方案
文檔原文:
在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較復雜且包含多個子值,或者下一個 state 依賴於之前的 state 等
從這個介紹上來看,使用 useReducer 在上面的場景中是比 useState 更合適的。
useReducer hook 是 react 的內置 hook,在聲明上如下:
const [state, dispatch] = useReducer(reducer, initialArg, init);
useReducer 本身接受的參數有三個:
- 一個 reducer:
(state, action) => newStat
(reducer 與 redux 中的概念其實一樣) - 初始化值
- init 是用來進行惰性初始化的:
init(initialArg)
如果使用 redux ,本身我們就不會直接操作 state,而是通過 dispatch 去觸發某些規則,因此 useReducer 本身也會返回一個 dispatch
1、聲明一個 reducer
下面的 reducer 比較簡單,處理了一下 increment
const reducer = (state, action) => { switch (action.type) { case 'increment': return { ...state, count: state.count + state.step, step: state.step + 1, } default: return state; } }
2、使用 useReducer 聲明 state 和 dispatch
const initialState = { count: 0, step: 1 } const [state, dispatch] = useReducer(reducer, initialState); const { count, step } = state;
3、使用 dispatch 進行 state 的一些變更
一開始提出的代碼改成下面:
useEffect(() => { console.log('render useEffect') const id = setInterval(() => { dispatch({ type: 'increment' }); console.log(`[] count is ${state.count}, step is ${state.step}`); }, 1000); return () => clearInterval(id); }, []);
4、效果:
首先我們實現了實例一次 setInterval 定時器,但是卻能夠時刻處理 count 和 step 的變化
在一次渲染閉包內,能夠每次訪問到 state 的最新值
5、依賴真的都誠實了么?
現在對 count 和 step 的兩個依賴都剝離出去了,我們認為目前 useEffect 的依賴都是誠實的,其實不然。
因為我們最終還是依賴了 dispatch
,不是只有 state 才叫依賴
但是我們都知道 dispatch 本身是不會變化的,因此我們認為對 uesEffect 來說,依賴都是誠實的
三、useCallback 解決 useEffect 內部函數的依賴誠實問題
1、非 useEffect 內部函數引起的依賴欺騙
上面代碼中我們發現,如果 dispatch 內部也依賴了某些變量,這個時候很容易造成依賴的欺騙問題。
為了解決這個問題,我們可能都需將其他函數寫在 useEffect 內部才能借助 eslint-plugin-react-hooks
這個插件檢查通過
可以針對思考下面代碼:
const [count, setCount] = useState(0); const [step, setStep] = useState(1); const setCountNew = () => { setCount(count + step); setStep(step + 1); } useEffect(() => { const tm = setInterval(() => { setCountNew(); console.log(count, step) }, 1000); return () => { clearInterval(tm); } }, []);
上面的代碼只是將 setCount 和 setStep 這樣的方法移到了 useEffect 外面,目前在 useEffect 中我們從代碼上看(忽略 console)是沒有 state 的依賴的,看起來是沒問題。
eslint 插件只會掃描出 setCountNew()
而上面的輸出結果只會輸出一次,即使我們有定時器。定時器是一致在執行的,但是頁面是不會變化的,因為每次在 setCountNew
的時候,拿到的 count 和 step 都是第一次渲染閉包的值,也就是 0 和 1
2、useCallback 解決依賴欺騙問題
有些情況下我們不能將函數都寫在 useEffect 內部,會造成無法管理,代碼也會臃腫。
useCallback 本身會返回一個方法,同時 useCallback 接收兩個參數:
- 參數1:匿名方法,里面執行相關的邏輯
- 參數2:數據依賴,本身 useCallback 需要監聽相關的依賴項,這些依賴項可以在上面的方法中使用
文檔的說明:
把內聯回調函數及依賴項數組作為參數傳入 useCallback,它將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時才會更新
上面方法的改造如下:
const [count, setCount] = useState(0); const [step, setStep] = useState(1); const setCountNew = useCallback(() => { setCount(count + step); setStep(step + 1); }, [count, step]); useEffect(() => { console.log('render useEffect') const tm = setInterval(() => { setCountNew(); }, 1000); return () => { clearInterval(tm); } }, [setCountNew]);
上面的改動中,除了我們使用 useCallback 聲明一個 setCountNew 的方法,並且在 useEffect 方法本身用之外,useEffect 還依賴了 setCountNew
。
這個表示說明,當 setCountNew 發生變化的時候(本身如果 state 發生了變化則返回的方法也會發生變化)
輸出結果:
我們可以發現,輸出結果中,每次都會重新執行 useEffect ,因為對於 useEffect 來說,useCallback 的 memorize 回調已經發生變化,基於此,我們可以放心的認為 useEffect 中依賴都是誠實的。