听说,这个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以前多执行一次形成问题的出现。