react hook——你可能不是“我”所认识的useEffect
據(jù)說(shuō),這個(gè)hook可以模擬class組件的三個(gè)生命周期
前言
官網(wǎng)已經(jīng)介紹過(guò),這里再啰嗦一次。useEffect是一個(gè)用來(lái)執(zhí)行副作用hook,第一個(gè)參數(shù)傳入一個(gè)函數(shù),每一次render之后執(zhí)行副作用和清除上一次副作用,該函數(shù)的返回值就是清除函數(shù)。第二個(gè)參數(shù)是一個(gè)數(shù)組,傳入內(nèi)部的執(zhí)行副作用函數(shù)需要的依賴,當(dāng)這幾個(gè)依賴有一個(gè)要更新,effect里面也會(huì)重新生成一個(gè)新的副作用并執(zhí)行副作用。如果沒(méi)有更新,則不會(huì)執(zhí)行。如果第二個(gè)參數(shù)不傳,那么就是沒(méi)有說(shuō)明自己有沒(méi)有依賴,那就是每次render該函數(shù)組件都執(zhí)行。
很明顯,useEffect第一個(gè)參數(shù)可以模仿didmount、didupdate,它的返回值可以模仿willunmount
class組件生命周期模擬
"模仿生命周期,useEffect第二個(gè)參數(shù)傳個(gè)空數(shù)組,無(wú)依賴,只執(zhí)行一次,相當(dāng)于didmount。如果要區(qū)分生命周期,不傳第二個(gè)參數(shù),每次都會(huì)跑,相當(dāng)于didupdate。加個(gè)mount標(biāo)記一下,里面用if判斷一下,即可以達(dá)到模擬生命周期的效果"
很多人都會(huì)想到這個(gè)辦法模擬,于是我們?cè)囈幌驴纯?#xff1a;
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>我是隨時(shí)被拋棄的<button onClick={forceUpdate}>強(qiáng)制更新</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>) } 復(fù)制代碼當(dāng)count是奇數(shù),那就展示UnmountTest,組件里面也有一個(gè)更新組件的方法。按照邏輯,useEffect不傳第二個(gè)參數(shù),保證每次渲染都執(zhí)行。然后加一個(gè)標(biāo)記,標(biāo)記第一次是掛載。于是運(yùn)行一波看看
- 點(diǎn)一下count+1,展示組件,打印didmount
- 再點(diǎn)一下count,刪掉組件,打印unmount
符合預(yù)期,?
- 點(diǎn)一下count+1,展示組件,打印didmount
- 點(diǎn)一下強(qiáng)制更新,打印unmount、didmount,再點(diǎn),還是一樣
??,什么鬼,居然不符合預(yù)期
useEffect是用來(lái)執(zhí)行副作用,每一次render,將會(huì)清除上一次副作用、執(zhí)行本次副作用(如果有依賴或者不傳入依賴數(shù)組)這個(gè)hook是以一個(gè)副作用為單位,當(dāng)然也可以多次使用
這樣子說(shuō),每一次都是unmount、didmount,的確是符合這個(gè)邏輯,和"想當(dāng)然"的那種模擬生命周期是有點(diǎn)不一樣的。這樣子,我們拆成兩個(gè)useEffect調(diào)用,就可以解決問(wèn)題:
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>我是隨時(shí)被拋棄的<button onClick={forceUpdate}>強(qiáng)制更新</button></div>); } 復(fù)制代碼這次,全都符合預(yù)期了,簡(jiǎn)直ojbk?
useEffect & useLayoutEffect區(qū)別
useEffect是異步的,useLayoutEffect是同步的
我們看一下,一次組件從掛載到重新渲染,兩者的發(fā)生的時(shí)機(jī):
從左到右表示時(shí)間線,紅色的是異步的,紅色框內(nèi)是同步的,從上到下執(zhí)行。useEffect是異步的,所謂的異步就是利用requestIdleCallback,在瀏覽器空閑時(shí)間執(zhí)行傳入的callback。大部分情況下,用哪一個(gè)都是一樣的,如果副作用執(zhí)行比較長(zhǎng),比如大量計(jì)算,如果是useLayoutEffect就會(huì)造成渲染阻塞。這只是一個(gè)case,我們可以看一下這個(gè)神奇的定時(shí)器:
點(diǎn)擊開(kāi)始,開(kāi)始計(jì)時(shí),點(diǎn)擊暫停就暫停。點(diǎn)擊清0,暫停并且數(shù)字清零
function LYE() {const [lapse, setLapse] = React.useState(0)const [running, setRunning] = React.useState(false)useEffect(() => {if (running) {const startTime = Date.now() - lapseconst 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 ? '暫停' : '開(kāi)始'}</button><button onClick={handleClearClick}>暫停并清0</button></div>) } 復(fù)制代碼于是,點(diǎn)擊清零居然不清0,只是停下來(lái)了,而且點(diǎn)開(kāi)始也是繼續(xù)開(kāi)始。這里只要把它改成useLayoutEffect就可以了,點(diǎn)清0馬上變成0并停止。另外,在使用useEffect下,把interval的時(shí)間改成大于16,有概率成功清0,如果更大一點(diǎn)是絕對(duì)清零。都說(shuō)useEffect是異步,那么問(wèn)題很有可能出現(xiàn)在異步這里。
useLayoutEffect是同步的,所以整個(gè)流程完全符合我們的預(yù)期,一切在掌控之中。基于兩點(diǎn): useEffect里面的interval延遲太小并沒(méi)有清除計(jì)時(shí)結(jié)果、useEffect把interval延遲調(diào)到大于16后有概率解決。我們從這兩點(diǎn)出發(fā),梳理一下useEffect執(zhí)行時(shí)機(jī):
這種情況是沒(méi)有清除定時(shí)器結(jié)果的,注意中間那塊:interval1 =》 render =》 clean useEffect1。 clean useEffect1之前又跑了一次interval1,interval1觸發(fā)render,展示的是當(dāng)前計(jì)時(shí)結(jié)果。前面的stop操作, setRunning(false)和setLapse(0)的確是跑了,但是interval1又設(shè)置了當(dāng)前計(jì)時(shí)結(jié)果,所以setLapse(0)就是白搞了。
把interval延遲調(diào)大
這種情況是正常的,顯然全部都在我們預(yù)期之內(nèi)。經(jīng)過(guò)多次測(cè)試,延遲臨界點(diǎn)是16ms。
為什么就是16ms?
有問(wèn)題,很自然想到異步,說(shuō)到異步又想到了requestIdleCallback,這個(gè)函數(shù)就是瀏覽器空閑的時(shí)候執(zhí)行callback。類似于requestAnimationFrame,只是requestIdleCallback把優(yōu)先級(jí)放低了。說(shuō)到requestAnimationFrame就想到了平均60fps,接著1000/60 就是16.66666,所以每一幀的間隔大約是16ms左右。最后,問(wèn)題來(lái)源就這樣暴露出來(lái)了,當(dāng)interval間隔大于屏幕一幀時(shí)間,用useEffect此定時(shí)器不會(huì)有問(wèn)題,反之則是interval會(huì)在useEffect之前多執(zhí)行一次造成問(wèn)題的出現(xiàn)。
如果文章對(duì)你有幫助,github , 掘金 可以關(guān)注一波哦
總結(jié)
以上是生活随笔為你收集整理的react hook——你可能不是“我”所认识的useEffect的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Web后台服务开发——数据库查询之引入T
- 下一篇: WEB前后端分离开发中的验证与安全问题