聽說,這個hook能夠模擬class組件的三個生命週期git
官網已經介紹過,這裏再囉嗦一次。useEffect
是一個用來執行反作用hook,第一個參數傳入一個函數,每一次render以後執行反作用和清除上一次反作用,該函數的返回值就是清除函數。第二個參數是一個數組,傳入內部的執行反作用函數須要的依賴,當這幾個依賴有一個要更新,effect裏面也會從新生成一個新的反作用並執行反作用。若是沒有更新,則不會執行。若是第二個參數不傳,那麼就是沒有說明本身有沒有依賴,那就是每次render該函數組件都執行。github
很明顯,useEffect
第一個參數能夠模仿didmount
、didupdate
,它的返回值能夠模仿willunmount
數組
"模仿生命週期,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
不傳第二個參數,保證每次渲染都執行。而後加一個標記,標記第一次是掛載。因而運行一波看看函數
符合預期,😊測試
🤔️,什麼鬼,竟然不符合預期ui
useEffect是用來執行反作用,每一次render,將會清除上一次反作用、執行本次反作用(若是有依賴或者不傳入依賴數組)這個hook是以一個反作用爲單位,固然也能夠屢次使用spa
這樣子說,每一次都是unmount、didmount,的確是符合這個邏輯,和"想固然"的那種模擬生命週期是有點不同的。這樣子,咱們拆成兩個useEffect調用,就能夠解決問題:3d
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
是異步的,所謂的異步就是利用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以前多執行一次形成問題的出現。