據說,這個hook可以模擬class元件的三個生命週期
前言
官網已經介紹過,這裡再囉嗦一次。useEffect
是一個用來執行副作用hook,第一個引數傳入一個函式,每一次render之後執行副作用和清除上一次副作用,該函式的返回值就是清除函式。第二個引數是一個陣列,傳入內部的執行副作用函式需要的依賴,當這幾個依賴有一個要更新,effect裡面也會重新生成一個新的副作用並執行副作用。如果沒有更新,則不會執行。如果第二個引數不傳,那麼就是沒有說明自己有沒有依賴,那就是每次render該函式元件都執行。
很明顯,useEffect
第一個引數可以模仿didmount
、didupdate
,它的返回值可以模仿willunmount
class元件生命週期模擬
"模仿生命週期,useEffect第二個引數傳個空陣列,無依賴,只執行一次,相當於didmount。如果要區分生命週期,不傳第二個引數,每次都會跑,相當於didupdate。加個mount標記一下,裡面用if判斷一下,即可以達到模擬生命週期的效果"
很多人都會想到這個辦法模擬,於是我們試一下看看:
let mount;
function useForceUpdate() {
const [_, forceUpdate] = useState(0);
return () => forceUpdate(x => x + 1);
}
function UnmountTest() {
useEffect(() => {
if (!mount) {
mount = true;
console.log('did mount')
} else {
console.log('did update')
}
return () => {
mount = false;
console.log('unmount')
}
})
const forceUpdate = useForceUpdate();
return (<div>
我是隨時被拋棄的
<button onClick={forceUpdate}>強制更新</button>
</div>);
}
function State() {
const [count, setCount] = useState(20);
const handleCount = useCallback(() => {
setCount(count => count + 1)
}, [])
return (
<div>
{count}
<button onClick={handleCount}>count+1</button>
{(count % 2) && <UnmountTest />}
</div>
)
}
複製程式碼
當count是奇數,那就展示UnmountTest
,元件裡面也有一個更新元件的方法。按照邏輯,useEffect
不傳第二個引數,保證每次渲染都執行。然後加一個標記,標記第一次是掛載。於是執行一波看看
- 點一下count+1,展示元件,列印didmount
- 再點一下count,刪掉元件,列印unmount
符合預期,?
- 點一下count+1,展示元件,列印didmount
- 點一下強制更新,列印unmount、didmount,再點,還是一樣
?️,什麼鬼,居然不符合預期
useEffect是用來執行副作用,每一次render,將會清除上一次副作用、執行本次副作用(如果有依賴或者不傳入依賴陣列)這個hook是以一個副作用為單位,當然也可以多次使用
這樣子說,每一次都是unmount、didmount,的確是符合這個邏輯,和"想當然"的那種模擬生命週期是有點不一樣的。這樣子,我們拆成兩個useEffect呼叫,就可以解決問題:
function UnmountTest() {
useEffect(() => {
if (mount) {
console.log('did update')
}
});
useEffect(() => {
if (!mount) {
console.log('did mount')
mount = true;
}
return () => {
console.log('unmount')
mount = false;
}
}, []);
const forceUpdate = useForceUpdate();
return (<div>
我是隨時被拋棄的
<button onClick={forceUpdate}>強制更新</button>
</div>);
}
複製程式碼
這次,全都符合預期了,簡直ojbk?
useEffect & useLayoutEffect區別
useEffect是非同步的,useLayoutEffect是同步的
我們看一下,一次元件從掛載到重新渲染,兩者的發生的時機:
從左到右表示時間線,紅色的是非同步的,紅色框內是同步的,從上到下執行。useEffect
是非同步的,所謂的非同步就是利用requestIdleCallback
,在瀏覽器空閒時間執行傳入的callback。大部分情況下,用哪一個都是一樣的,如果副作用執行比較長,比如大量計算,如果是useLayoutEffect
就會造成渲染阻塞。這只是一個case,我們可以看一下這個神奇的定時器:
點選開始,開始計時,點選暫停就暫停。點選清0,暫停並且數字清零
function LYE() {
const [lapse, setLapse] = React.useState(0)
const [running, setRunning] = React.useState(false)
useEffect(
() => {
if (running) {
const startTime = Date.now() - lapse
const intervalId = setInterval(() => {
setLapse(Date.now() - startTime)
}, 2)
console.log(intervalId)
return () => clearInterval(intervalId)
}
},
[running],
)
function handleRunClick() {
setRunning(r => !r)
}
function handleClearClick() {
setRunning(false)
setLapse(0)
}
return (
<div>
<label>{lapse}ms</label>
<button onClick={handleRunClick}>
{running ? '暫停' : '開始'}
</button>
<button onClick={handleClearClick}>
暫停並清0
</button>
</div>
)
}
複製程式碼
於是,點選清零居然不清0,只是停下來了,而且點開始也是繼續開始。這裡只要把它改成useLayoutEffect
就可以了,點清0馬上變成0並停止。另外,在使用useEffect
下,把interval的時間改成大於16,有概率成功清0,如果更大一點是絕對清零。都說useEffect
是非同步,那麼問題很有可能出現在非同步這裡。
useLayoutEffect
是同步的,所以整個流程完全符合我們的預期,一切在掌控之中。基於兩點: useEffect
裡面的interval延遲太小並沒有清除計時結果、useEffect
把interval延遲調到大於16後有概率解決。我們從這兩點出發,梳理一下useEffect
執行時機:
這種情況是沒有清除定時器結果的,注意中間那塊:interval1 =》 render =》 clean useEffect1。 clean useEffect1之前又跑了一次interval1,interval1觸發render,展示的是當前計時結果。前面的stop操作, setRunning(false)
和setLapse(0)
的確是跑了,但是interval1又設定了當前計時結果,所以setLapse(0)
就是白搞了。
把interval延遲調大
這種情況是正常的,顯然全部都在我們預期之內。經過多次測試,延遲臨界點是16ms。
為什麼就是16ms?
有問題,很自然想到非同步,說到非同步又想到了requestIdleCallback
,這個函式就是瀏覽器空閒的時候執行callback。類似於requestAnimationFrame
,只是requestIdleCallback
把優先順序放低了。說到requestAnimationFrame
就想到了平均60fps,接著1000/60 就是16.66666,所以每一幀的間隔大約是16ms左右。最後,問題來源就這樣暴露出來了,當interval間隔大於螢幕一幀時間,用useEffect
此定時器不會有問題,反之則是interval會在useEffect之前多執行一次造成問題的出現。